springboot学习01 – 自定义自动配置

概述

SpringBoot提供了自动配置能力。通过自动配置我们可以非常方便地启动相关的服务。

SpringBoot自动配置有两个核心模块:

  1. 自动配置模块( autoconfigure ):主要负责读取配置相关的内容,并尝试启动服务;
  2. 启动模块( 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默认提供的名称空间( servermanagementspring 等等)产生冲突。建议使用自己的关键字作为名称空间,比如我的组件名称是 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文件。我们只需要添加如下依赖即可:

<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 实例注入完成后执行了一行输出语句。可以说是极为简单了。

测试

自动配置可能会被多种因素影响:

  • 用户自定义配置(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 ,即条件注解检查过程日志。日志的级别可以设置为 INFODEBUG ,下面的测试代码使用了 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!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章