SpringBoot提供了自动配置能力。通过自动配置我们可以非常方便地启动相关的服务。
SpringBoot自动配置有两个核心模块:
通常这两个模块是分开的。比如使用Caffeine缓存,缓存自动配置在一个独立的包中,Caffine缓存支持又是一个独立的包。如果不想把配置和能力分开,这两个模块也可以放在一起。
接下来尝试创建一个自启动配置组件:功能很简单,就是在服务启动后自动打印一行“Hello xxx!”。
springboot官方的自动配置包和自启动包都是以“ spring-boot- ”开头的。但是springboot不建议第三方开发者这样命名,应该是担心和官方支持出现冲突——即使现在没有冲突,未必以后官方不会推出相同的服务。即使使用了不同的groupId也仍然不建议这么做。
官方的建议是将具体的名称放“ spring-boot ”在前面。比如,我们要创建一个名为 hello 的自动配置组件,那么自动配置模块包可以命名为“ hello-spring-boot-autoconfigure ”,自启动模块包可以命名为“ hello-spring-boot-starter ”。如果要把这两个模块合并起来,那么包名是“ hello-spring-boot-starter ”。
如果自定义的自动配置组件提供了配置项,那么需要为配置项提供一个独立的名称空间。注意,尽量不要和spring-boot默认提供的名称空间( server 、 management 、 spring 等等)产生冲突。建议使用自己的关键字作为名称空间,比如我的组件名称是 hello ,那么配置项就是:
hello: name: zhyea
然后需要为这些配置项创建一个配置描述类,如:
@ConfigurationProperties("hello") public class HelloProperties { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
配置描述类中需要包含全部配置项,以确保其生效。
下面是一些SpringBoot内部的配置项创建的准则:
为了能让idea等开发工具识别我们提供的配置项,还需要提供一个meta-data文件 META-INF/spring-configuration-metadata.json 。
SpringBoot提供了 annotationProcessor 来辅助生成meta-data文件。我们只需要添加如下依赖即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency>
不过 annotationProcessor 对集合类型支持得不是很好,使用的时候要慎重。
此外, annotationProcessor 还能生成一个配置项元数据文件 META-INF/spring-autoconfigure-metadata.properties 。当存在这个文件的时候,就可以了用来对配置项进行初步的过滤,有助于减少启动耗时。
自动配置组件的配置类就是一个标准的配置类,所以它也需要使用 @ Configuration 注解。下面是一个配置类的示例:
@Configuration @ConditionalOnClass(System.class) @EnableConfigurationProperties(HelloProperties.class) public class HelloAutoConfiguration { private HelloProperties helloProperties; public HelloAutoConfiguration(HelloProperties helloProperties) { this.helloProperties = helloProperties; } @Bean public HelloStarter helloStarter() { return new HelloStarter(helloProperties.getName()); } }
示例代码中通过 @ EnableConfigurationProperties 注解引入了配置描述类。还提供了相应的构造器以便注入配置信息。
此外这里还装模作样的使用了条件注解 @ ConditionalOnClass 。 System . class 是JRE的标配,因此这行注解实际上是没有任何作用的,在这里只是做个演示。条件注解通常多出现在自动配置中,以保证在满足设定条件后自动配置才能生效。关于条件注解前段时间写过一篇文:《 SpringBoot条件注解 》。这里就不重复啰嗦了。
因为自动配置组件要求放在独立的包中,而且包路径不能和应用包路径重合,所以需要提供一些帮助才能让SpringBoot识别我们提供的自动配置信息——这里是 META-INF/spring.factories 文件。SpringBoot会检查jar包中是否存在 META-INF/spring.factories 文件,并尝试读取解析文件中配置的类信息。关于读取解析 spring.factories 文件的过程在之前也有介绍过:《 SpringBoot启动过程之getSpringFactoriesInstances 》。
下面是一个 spring.factories 文件的示例:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=/ org.chobit.spring.autoconfig.HelloAutoConfiguration,/ org.chobit.spring.autoconfig.HelloAutoConfiguration2
应该可以看出 spring.factories 实际上就是一个典型的 . properties 文件。
注意:SpringBoot自动配置组件只能通过这种形式加载。在定义组件包路径的时候就需要注意包路径不能是Spring componentScan的目标。同时,在自定义组件类中也不能使用componentScan来获取其它的组件。如有必要,可以使用 @ Import 注解代替(可以参考 SpringBoot探索01- @ Import 注解 )。
如果多个配置类之间存在先后顺序的话,可以使用 @ AutoConfigureAfter 和 @ AutoConfigureBefore 注解来确定顺序。比如,如果定义的是web相关的配置类,那么这个配置类可能就需要在 WebMvcAutoConfiguration 之后生效。
如果想保证多个配置类的加载顺序,又不想让配置类之间存在显式的关联,那么可以使用 @ AutoConfigureOrder 注解。这个注解和普通的 @ Order 注解的作用是一样的,但是只能用于自动配置类。
关于启动类的作用,根据名称就可以猜出来:主要是负责组件服务的启动。前面配置类的示例代码中就有几行启动类相关的内容:
@Bean public HelloStarter helloStarter() { return new HelloStarter(helloProperties.getName()); }
其中的 HelloStarter 就是一个启动类。在配置类中创建注入了 HelloStarter 的实例。具体的服务逻辑还是需要在启动类 HelloStarter 中完成。
很多时候,启动模块和配置模块是分别放在独立的包中的,不过这里实现的功能比较简单,且无其它的依赖,所以就干脆放在一个jar中了。
看下 HelloStarter 的实现:
[crayon-5e2271731af4a045887038 inline="true" class="hljs"]<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HelloStarter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">InitializingBean</span> </span>{ <span class="hljs-keyword">private</span> String name; <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">HelloStarter</span><span class="hljs-params">(String name)</span> </span>{ <span class="hljs-keyword">this</span>.name = name; } <span class="hljs-meta">@Override</span> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterPropertiesSet</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> Exception </span>{ System.out.println(<span class="hljs-string">"Hello "</span> + name + <span class="hljs-string">"!"</span>); } }
[/crayon]
只是在 HelloStarter 实例注入完成后执行了一行输出语句。可以说是极为简单了。
自动配置可能会被多种因素影响:
执行具体测试的时候就需要为每种情形定义一个 ApplicationContext 。这种情况下,使用 ApplicationContextRunner 事情会变得很简单。
ApplicationContextRunner 主要被用来搜集基础的、通用的配置信息。通常是作为成员变量定义在测试类中,如下例:
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HelloAutoConfiguration.class));
如果定义了多个配置类,不用在测试中刻意控制声明的顺序,SpringBoot会保证它们的触发顺序和正常启动时一致。
每个测试都可以使用contextRunner执行一类测试案例。在下面的示例代码中定义了一个新的配置类,但是在新的配置类中创建的 HelloStarter Bean并不能覆盖自动配置中创建的同类的Bean:
@Test public void defaultStarterBacksOff() { this.contextRunner.withUserConfiguration(HelloConfiguration.class).run((context) -> { assertThat(context).hasSingleBean(HelloStarter.class); assertThat(context).getBean("helloStarter").isSameAs(context.getBean(HelloStarter.class)); assertThat(context.getBean(HelloStarter.class).getName()).isEqualTo(null); }); } @Configuration static class HelloConfiguration { @Bean HelloStarter helloStarter() { return new HelloStarter("chobit"); } }
因为没有提供配置信息,所以自动配置中创建的 HelloStarter Bean的name值是null。
在测试中使用了Assert4J来进行值的比较。
还可以自定义配置参数,如下:
@Test public void serviceNameCanBeConfigured() { this.contextRunner.withPropertyValues("hello.name=chobit").run((context) -> { assertThat(context.getBean(HelloStarter.class).getName()).isEqualTo("chobit"); }); }
contextRunne还可以展示 ConditionEvaluationReport ,即条件注解检查过程日志。日志的级别可以设置为 INFO 或 DEBUG ,下面的测试代码使用了 ConditionEvaluationReportLoggingListener 来打印条件注解检查过程日志:
@Test public void autoConfigTest() { ConditionEvaluationReportLoggingListener initializer = new ConditionEvaluationReportLoggingListener( LogLevel.INFO); ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HelloAutoConfiguration.class)) .withInitializer(initializer).run((context) -> System.out.println(context.getBean(HelloStarter.class).getName())); }
借助于SpringBoot提供的 FilteredClassLoader ,我们还能够验证在某个类或某个jar不存在的情况下自动配置如何处理。在下面的代码中,我们在类加载器中排除掉了 HelloStarter . class ,这样自动配置就不会生效:
@Test public void serviceIsIgnoredIfLibraryIsNotPresent() { this.contextRunner.withClassLoader(new FilteredClassLoader(HelloStarter.class)) .run((context) -> assertThat(context).doesNotHaveBean("helloStarter")); }
另外,如果我们需要的是Servlet或Reactive web应用Context,可以使用 WebApplicationContextRunner 或者 ReactiveWebApplicationContextRunner 。
这里的测试代码已经上传到了GitHub,见: GitHub/zhyea 。
不过这个自启动组件实现的功能太过简单,如果想深入了解下,可以参考SpringBoot官方提供的自启动配置。我自己还写过一个 简易的kafka自启动组件 ,如果有兴趣也可以参考下。
还有一个自动配置演示项目也不错,在git: spring-boot-master-auto-configuration
End!