文章目录
概述
SpringBoot提供了自动配置能力。通过自动配置我们可以非常方便地启动相关的服务。
SpringBoot自动配置有两个核心模块:
- 自动配置模块(autoconfigure):主要负责读取配置相关的内容,并尝试启动服务;
- 启动模块(starter):提供具体的服务能力以及所有相关的依赖。
通常这两个模块是分开的。比如使用Caffeine缓存,缓存自动配置在一个独立的包中,Caffine缓存支持又是一个独立的包。如果不想把配置和能力分开,这两个模块也可以放在一起。
创建
接下来尝试创建一个自启动配置组件:hello-spring-boot-starter。这个组件功能很简单,就是在服务启动后自动打印一行“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,最终yaml格式的配置就是:
1 2 |
hello: name: zhyea |
然后需要为这些配置项创建一个配置描述类来表示记录配置信息,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@ConfigurationProperties("hello") public class HelloProperties { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } |
配置描述类中需要包含全部配置项,以确保其生效。
下面是一些SpringBoot内部的配置类定义的准则:
- 不以“the”或“a”开头
- boolean类型的配置项,以“weather”或“enable”开头
- 对于简单集合类型,尽量使用逗号分隔的字符串形式
- 对于毫秒级的时间,使用
java.time.Duration
替换long
类型 - 如果时间不是毫秒级的,需要在meta-data中提供必要的提示,如:”If a duration suffix is not specified, seconds will be used”
- 提供默认值要谨慎,如果默认值不是在运行时必需的就不要设置
为了能让idea等开发工具识别我们提供的配置项,还需要提供一个meta-data文件META-INF/spring-configuration-metadata.json。
SpringBoot提供了annotationProcessor来辅助生成meta-data文件。我们只需要添加如下依赖即可:
1 2 3 4 5 |
<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
注解。下面是一个配置类的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@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文件的示例:
1 2 3 |
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
注解的作用是一样的,但是只能用于自动配置类。
启动类
关于启动类的作用,根据名称就可以猜出来:主要是负责组件服务的启动。前面配置类的示例代码中就有几行启动类相关的内容:
1 2 3 4 |
@Bean public HelloStarter helloStarter() { return new HelloStarter(helloProperties.getName()); } |
其中的HelloStarter
就是一个启动类。在配置类中创建注入了HelloStarter
的实例。具体的服务逻辑还是需要在启动类HelloStarter
中完成。
很多时候,启动模块和配置模块是分别放在独立的包中的,不过这里实现的功能比较简单,且无其它的依赖,所以就干脆放在一个jar中了。
看下HelloStarter
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HelloStarter implements InitializingBean { private String name; public HelloStarter(String name) { this.name = name; } @Override public void afterPropertiesSet() throws Exception { System.out.println("Hello " + name + "!"); } } |
只是在HelloStarter
实例注入完成后执行了一行输出语句。可以说是极为简单了。
测试
自动配置可能会被多种因素影响:
- 用户自定义配置(Bean定义和自定义环境参数)
- 条件分析(是否存在某个类或某个依赖)
- 其它约束
执行具体测试的时候就需要为每种情形定义一个ApplicationContext
。这种情况下,使用ApplicationContextRunner
事情会变得很简单。
ApplicationContextRunner
主要被用来搜集基础的、通用的配置信息。通常是作为成员变量定义在测试类中,如下例:
1 2 |
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HelloAutoConfiguration.class)); |
如果定义了多个配置类,不用在测试中刻意控制声明的顺序,SpringBoot会保证它们的触发顺序和正常启动时一致。
每个测试都可以使用contextRunner执行一类测试案例。在下面的示例代码中定义了一个新的配置类HelloConfiguration ,但是在新的配置类中创建的HelloStarter
Bean并不能覆盖自动配置中创建的同类的Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@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来进行值的比较。
还可以自定义配置参数,如下:
1 2 3 4 5 6 |
@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
来打印条件注解检查过程日志:
1 2 3 4 5 6 7 8 9 10 |
@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
,这样自动配置就不会生效:
1 2 3 4 5 |
@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!
发表评论