springboot学习01 – 自定义自动配置
概述
SpringBoot提供了自动配置能力。通过自动配置我们可以非常方便地启动相关的服务。
SpringBoot自动配置有两个核心模块:
-
自动配置模块( autoconfigure
):主要负责读取配置相关的内容,并尝试启动服务; -
启动模块( starter
):提供具体的服务能力以及所有相关的依赖。
通常这两个模块是分开的。比如使用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内部的配置项创建的准则:
- 不以“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文件。我们只需要添加如下依赖即可:
org.springframework.boot spring-boot-autoconfigure-processor true
不过 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"]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 + "!");
}
}
[/crayon]
只是在
HelloStarter
实例注入完成后执行了一行输出语句。可以说是极为简单了。
测试
自动配置可能会被多种因素影响:
- 用户自定义配置(Bean定义和自定义环境参数)
- 条件分析(是否存在某个类或某个依赖)
- 其它约束
执行具体测试的时候就需要为每种情形定义一个
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!