49、创建自己的自动配置

如果您在公司负责开发公共类库,或者如果您在开发一个开源或商业库,您可能希望开发自己的自动配置。自动配置类可以捆绑在外部 jar 中,他仍然可以被 Spring Boot 获取。

自动配置可以与提供自动配置代码的 starter 以及您将使用的类库库相关联。我们首先介绍构建自己的自动配置需要了解的内容,然后我们将继续介绍创建自定义 starter 所需的步骤

提示

这里有一个演示项目展示了如何逐步创建 starter。

49.1、理解自定配置 Bean

在内部,自动配置使用了标准的 @Configuration 类来实现。@Conditional 注解用于约束何时应用自动配置。通常,自动配置类使用 @ConditionalOnClass@ConditionalOnMissingBean 注解。这可确保仅在找到相关类时以及未声明您自己的 @Configuration 时才应用自动配置。

您可以浏览 spring-boot-autoconfigure 的源代码,以查看 Spring 提供的 @Configuration 类(请参阅 META-INF/spring.factories 文件)。

49.2、找到候选的自动配置

Spring Boot 会检查已发布 jar 中是否存在 META-INF/spring.factories 文件。该文件应列出 EnableAutoConfiguration key 下的配置类,如下所示:

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2. com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
  3. com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

注意

必须以这种方式加载自动配置。确保它们在特定的包空间中定义,并且它们不能是组件扫描的目标。此外,自动配置类不应启用组件扫描以查找其他组件。应该使用特定的@Imports 来代替。

如果需要按特定顺序应用配置,则可以使用 @AutoConfigureAfter@AutoConfigureBefore 注解。例如,如果您提供特定于 Web 的配置,则可能需要在WebMvcAutoConfiguration 之后应用您的类。

如果您想排序某些不应该彼此直接了解的自动配置,您也可以使用 @AutoConfigureOrder。该注解与常规 @Order 注解有相同的语义,但它为自动配置类提供了专用顺序。

49.3、条件注解

您几乎总希望在自动配置类中包含一个或多个 @Conditional 注解。@ConditionalOnMissingBean 是一个常用的注解,其允许开发人员在对您的默认值不满意用于覆盖自动配置。

Spring Boot 包含许多 @Conditional 注解,您可以通过注解 @Configuration 类或单独的 @Bean 方法在您自己的代码中复用它们。这些注解包括:

49.3.1、类条件

@ConditionalOnClass@ConditionalOnMissingClass 注解允许根据特定类的是否存在来包含 @Configuration 类。由于使用 ASM 解析注解元数据,您可以使用 value 属性来引用真实类,即使该类实际上可能不会出现在正在运行的应用程序的 classpath 中。如果您希望使用 String 值来指定类名,也可以使用 name 属性。

此机制不会以相同的方式应用于返回类型是条件的目标的 @Bean 方法:在方法上的条件应用之前,JVM 将加载类和可能处理的方法引用,如果找不到类,将发生失败。

要处理这种情况,可以使用单独的 @Configuration 类来隔离条件,如下所示:

  1. @Configuration
  2. // Some conditions
  3. public class MyAutoConfiguration {
  4. // Auto-configured beans
  5. @Configuration
  6. @ConditionalOnClass(EmbeddedAcmeService.class)
  7. static class EmbeddedConfiguration {
  8. @Bean
  9. @ConditionalOnMissingBean
  10. public EmbeddedAcmeService embeddedAcmeService() { ... }
  11. }
  12. }

提示

如果使用 @ConditionalOnClass@ConditionalOnMissingClass 作为元注解的一部分来组成自己的组合注解,则必须使用 name 来引用类,在这种情况将不作处理。

49.3.2、Bean 条件

@ConditionalOnBean@ConditionalOnMissingBean 注解允许根据特定 bean 是否存在来包含 bean。您可以使用 value 属性按类型或使用 name 来指定 bean。search 属性允许您限制在搜索 bean 时应考虑的 ApplicationContext 层次结构。

放置在 @Bean 方法上时,目标类型默认为方法的返回类型,如下所示:

  1. @Configuration
  2. public class MyAutoConfiguration {
  3. @Bean
  4. @ConditionalOnMissingBean
  5. public MyService myService() { ... }
  6. }

在前面的示例中,如果 ApplicationContext 中不包含 MyService 类型的 bean,则将创建 myService bean。

提示

您需要非常小心地添加 bean 定义的顺序,因为这些条件是根据到目前为止已处理的内容进行计算的。因此,我们建议在自动配置类上仅使用 @ConditionalOnBean@ConditionalOnMissingBean 注解(因为这些注解保证在添加所有用户定义的 bean 定义后加载)。

注意

@ConditionalOnBean@ConditionalOnMissingBean 不会阻止创建 @Configuration 类。在类级别使用这些条件并使用注解标记每个包含 @Bean 方法的唯一区别是,如果条件不匹配,前者会阻止将 @Configuration 类注册为 bean。

49.3.3、属性条件

@ConditionalOnProperty 注解允许基于 Spring Environment 属性包含配置。使用 prefixname 属性指定需要检查的属性。默认情况下,匹配存在且不等于 false 的所有属性。您还可以使用 havingValuematchIfMissing 属性创建更高级的检查。

49.3.4、资源条件

@ConditionalOnResource 注解仅允许在存在特定资源时包含配置。可以使用常用的 Spring 约定来指定资源,如下所示:file:/home/user/test.dat

49.3.5、Web 应用程序条件

@ConditionalOnWebApplication@ConditionalOnNotWebApplication 注解在应用程序为 Web 应用程序的情况下是否包含配置。Web 应用程序是使用 Spring WebApplicationContext,定义一个 session 范围或具有 StandardServletEnvironment 的任何应用程序。

49.3.6、SpEL 表达式条件

@ConditionalOnExpression 注解允许根据 SpEL 表达式的结果包含配置。

49.4、测试自动配置

自动配置可能受许多因素的影响:用户配置(@Bean 定义和 Environment 自定义)、条件评估(存在特定的类库)等。具体而言,每个测试都应该创建一个定义良好的 ApplicationContext,它表示这些自定义的组合。ApplicationContextRunner 提供了一个好的实现方法。

ApplicationContextRunner 通常被定义为测试类的一个字段,用于收集基本的通用配置。以下示例确保始终调用 UserServiceAutoConfiguration

  1. private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
  2. .withConfiguration(AutoConfigurations.of(UserServiceAutoConfiguration.class));

提示

如果必须定义多个自动配置,则无需按照与运行应用程序时完全相同的顺序调用它们的声明。

每个测试都可以使用 runner 来表示特定的用例。例如,下面的示例调用用户配置(UserConfiguration)并检查自动配置是否正确退回。调用 run 提供了一个可以与 Assert4J 一起使用的回调上下文。

  1. @Test
  2. public void defaultServiceBacksOff() {
  3. this.contextRunner.withUserConfiguration(UserConfiguration.class)
  4. .run((context) -> {
  5. assertThat(context).hasSingleBean(UserService.class);
  6. assertThat(context.getBean(UserService.class)).isSameAs(
  7. context.getBean(UserConfiguration.class).myUserService());
  8. });
  9. }
  10. @Configuration
  11. static class UserConfiguration {
  12. @Bean
  13. public UserService myUserService() {
  14. return new UserService("mine");
  15. }
  16. }

也可以轻松自定义 Environment,如下所示:

  1. @Test
  2. public void serviceNameCanBeConfigured() {
  3. this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
  4. assertThat(context).hasSingleBean(UserService.class);
  5. assertThat(context.getBean(UserService.class).getName()).isEqualTo("test123");
  6. });
  7. }

runner 还可用于展示 ConditionEvaluationReport。报告可以在 INFODEBUG 级别下打印。以下示例展示如何使用 ConditionEvaluationReportLoggingListener 在自动配置测试中打印报表。

  1. @Test
  2. public void autoConfigTest {
  3. ConditionEvaluationReportLoggingListener initializer = new ConditionEvaluationReportLoggingListener(
  4. LogLevel.INFO);
  5. ApplicationContextRunner contextRunner = new ApplicationContextRunner()
  6. .withInitializer(initializer).run((context) -> {
  7. // Do something...
  8. });
  9. }

49.4.1、模拟一个 Web 上下文

如果需要测试一个仅在 Servlet 或响应式 Web 应用程序上下文中运行的自动配置,请分别使用 WebApplicationContextRunnerReactiveWebApplicationContextRunner

49.4.2、覆盖 Classpath

还可以测试在运行时不存在特定类和/或包时发生的情况。 Spring Boot附带了一个可以由跑步者轻松使用的FilteredClassLoader。 在以下示例中,我们声明如果UserService不存在,则会正确禁用自动配置:

  1. @Test
  2. public void serviceIsIgnoredIfLibraryIsNotPresent() {
  3. this.contextRunner.withClassLoader(new FilteredClassLoader(UserService.class))
  4. .run((context) -> assertThat(context).doesNotHaveBean("userService"));
  5. }

49.5、创建自己的 Starter

一个完整的 Spring Boot starter 类库可能包含以下组件:

  • autoconfigure 模块,包含自动配置代码。
  • starter 模块,它提供对 autoconfigure 模块依赖关系以及类库和常用的其他依赖关系。简而言之,添加 starter 应该提供该库开始使用所需的一切。

提示

如果您不想将这两个模块分开,则可以将自动配置代码和依赖关系管理组合在一个模块中。

49.5.1、命名

您应该确保为您的 starter 提供一个合适的命名空间。即使您使用其他 Maven groupId,也不要使用 spring-boot 作为模块名称的开头。我们可能会为您以后自动配置的内容提供官方支持。

根据经验,您应该在 starter 后命名一个组合模块。例如,假设您正在为 acme 创建一个 starter,并且您将自动配置模块命名为 acme-spring-boot-autoconfigure,将 starter 命名为 acme-spring-boot-starter。如果您只有一个组合这两者的模块,请将其命名为 acme-spring-boot-starter

此外,如果您的 starter 提供配置 key,请为它们使用唯一的命名空间。尤其是,不要将您的 key 包含在 Spring Boot 使用的命名空间中(例如 servermanagementspring 等)。如果您使用了相同的命名空间,我们将来可能会以破坏您的模块的方式来修改这些命名空间。

保触发元数据生成,以便为您的 key 提供 IDE 帮助。您可能想查看生成的元数据(META-INF/spring-configuration-metadata.json)以确保您的 key 记录是否正确。

49.5.2、autoconfigure 模块

autoconfigure 模块包含类库开始使用所需的所有内容。它还可以包含配置 key 定义(例如 @ConfigurationProperties)和任何可用于进一步自定义组件初始化方式的回调接口。

提示

您应该将类库的依赖项标记为可选,以便您可以更轻松地在项目中包含 autoconfigure 模块。如果以这种方式执行,则不提供类库,默认情况下,Spring Boot 将会退出。

Spring Boot 使用注解处理器来收集元数据文件(META-INF/spring-autoconfigure-metadata.properties)中自动配置的条件。如果该文件存在,则用于快速过滤不匹配的自动配置,缩短启动时间。建议在包含自动配置的模块中添加以下依赖项:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-autoconfigure-processor</artifactId>
  4. <optional>true</optional>
  5. </dependency>

使用 Gradle 4.5 及更早版本时,应在 compileOnly 配置中声明依赖项,如下所示:

  1. dependencies {
  2. compileOnly "org.springframework.boot:spring-boot-autoconfigure-processor"
  3. }

使用 Gradle 4.6 及更高版本时,应在 annotationProcessor 配置中声明依赖项,如下所示:

  1. dependencies {
  2. annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
  3. }

49.5.3、Starter 模块

starter 真的是一个空 jar。它的唯一目的是为使用类库提供必要的依赖项。您可以将其视为使用类库的一切基础。

不要对添加 starter 的项目抱有假设想法。如果您自动配置的库经常需要其他 starter,请一并声明它们。如果可选依赖项的数量很多,则提供一组适当的默认依赖项可能很难,因为您本应该避免包含对常用库的使用不必要的依赖项。换而言之,您不应该包含可选的依赖项。

注意

无论哪种方式,您的 starter 必须直接或间接引用核心 Spring Boot starter(spring-boot-starter)(如果您的 starter 依赖于另一个 starter ,则无需添加它)。如果只使用自定义 starter 创建项目,则 Spring Boot 的核心功能将通过存在的核心 starter 来实现。