Spring Boot 创建你自己的自动配置中文文档

本文为官方文档直译版本。原文链接

引言

如果您在开发共享库的公司工作,或者如果您在开发开源或商业库,您可能想开发自己的自动配置。自动配置类可以捆绑在外部 jar 中,但仍会被 Spring Boot 接收。
自动配置可以与 “starter” 相关联,“starter” 提供自动配置代码以及与之配合使用的典型库。我们首先介绍构建自己的自动配置所需的知识,然后介绍创建自定义启动器所需的典型步骤。

了解自动配置 Bean

实现自动配置的类用 @AutoConfiguration 进行注解。该注解本身用 @Configuration 元注解,使自动配置成为标准的 @Configuration 类。附加的 @Conditional 注解用于限制何时应用自动配置。通常,自动配置类使用 @ConditionalOnClass@ConditionalOnMissingBean 注解。这样可以确保自动配置仅在找到相关类且未声明自己的 @Configuration 时才会应用。
您可以浏览 spring-boot-autoconfigure 的源代码,查看 Spring 提供的 @AutoConfiguration 类(参见 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件)。

查找自动配置候选对象

Spring Boot 会检查发布的 jar 中是否存在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。该文件应列出配置类,每行一个类名,如下例所示:

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration

您可以使用 # 字符为导入文件添加注释。

自动配置必须通过在导入文件中命名的方式加载。确保它们被定义在特定的包空间中,并且永远不会成为组件扫描的目标。此外,自动配置类不应允许组件扫描查找其他组件。应使用特定的 @Import 注解来代替。

如果需要按特定顺序应用配置,可以使用 @AutoConfiguration 注解或专用的 @AutoConfigureBefore@AutoConfigureAfter 注解中的 beforebeforeNameafterafterName 属性。例如,如果您提供特定于 Web 的配置,您的类可能需要在 WebMvcAutoConfiguration 之后应用。
如果您想对某些不应直接相互了解的自动配置进行排序,也可以使用 @AutoConfigureOrder。该注解与常规的 @Order 注解语义相同,但为自动配置类提供了专用的顺序。
与标准的 @Configuration 类一样,应用自动配置类的顺序只影响定义其 Bean 的顺序。随后创建这些 Bean 的顺序不受影响,而是由每个 Bean 的依赖关系和任何 @DependsOn 关系决定。

条件注释

您几乎总是希望在自动配置类中包含一个或多个 @Conditional 注解。@ConditionalOnMissingBean 注解就是一个常见的例子,它允许开发人员在对默认值不满意时覆盖自动配置。
Spring Boot 包含大量 @Conditional 注解,您可以通过注解 @Configuration 类或单个 @Bean 方法在自己的代码中重复使用这些注解。这些注解包括

  • Class Conditions
  • Bean Conditions
  • Property Conditions
  • Resource Conditions
  • Web Application Conditions
  • SpEL Expression Conditions

Class Conditions

@ConditionalOnClass@ConditionalOnMissingClass 注解使 @Configuration 类可以根据特定类的存在或不存在而被包含。由于注解元数据是通过 ASM 解析的,因此您可以使用 value 属性来引用真正的类,即使该类实际上可能不会出现在运行应用程序的类路径中。如果希望使用字符串值指定类名,也可以使用 name 属性。
@Bean 方法中,返回类型通常是条件的目标:在方法上的条件适用之前,JVM 将加载类并处理方法引用,如果类不存在,方法引用将失败。
要处理这种情况,可以使用单独的 @Configuration 类来隔离条件,如下例所示:

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
// Some conditions ...
public class MyAutoConfiguration {

    // Auto-configured beans ...

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(SomeService.class)
    public static class SomeServiceConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public SomeService someService() {
            return new SomeService();
        }

    }

}

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

Bean Conditions

@ConditionalOnBean@ConditionalOnMissingBean 注解允许根据特定 Bean 的存在或不存在来包含 Bean。您可以使用 value 属性按类型指定 Bean,也可以使用 name 属性按名称指定 Bean。search属性可让您限制在搜索 Bean 时应考虑的 ApplicationContext 层次结构。
@Bean 方法中使用时,目标类型默认为方法的返回类型,如下例所示:

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public SomeService someService() {
        return new SomeService();
    }

}

在前面的示例中,如果 ApplicationContext 中尚未包含 SomeService 类型的 Bean,则将创建 someService Bean。

您需要非常注意添加 Bean 定义的顺序,因为这些条件是根据目前已处理的内容进行评估的。因此,我们建议在自动配置类中只使用 @ConditionalOnBean@ConditionalOnMissingBean 注解(因为这些注解保证在添加任何用户定义的 Bean 定义后加载)。

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

在声明 @Bean 方法时,请在方法的返回类型中提供尽可能多的类型信息。例如,如果 Bean 的具体类实现了一个接口,那么 Bean 方法的返回类型就应该是具体类而不是接口。在 @Bean 方法中提供尽可能多的类型信息在使用 Bean 条件时尤为重要,因为对这些条件的评估只能依赖于方法签名中可用的类型信息。

Property Conditions

@ConditionalOnProperty 注解允许根据 Spring 环境属性进行配置。使用prefixname属性指定应检查的属性。默认情况下,任何存在且不等于 false 的属性都会被匹配。您还可以使用 havingValuematchIfMissing 属性创建更高级的检查。

Resource Conditions

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

Web Application Conditions

通过 @ConditionalOnWebApplication 和 @ConditionalOnNotWebApplication 注解,可以根据应用程序是否是 Web 应用程序来进行配置。基于 servlet 的 Web 应用程序是指使用 Spring WebApplicationContext、定义session作用域或具有 ConfigurableWebEnvironment 的任何应用程序。反应式网络应用是指使用 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的任何应用。
通过 @ConditionalOnWarDeployment@ConditionalOnNotWarDeployment 注解,可以根据应用程序是否是部署到 servlet 容器的传统 WAR 应用程序来进行配置。对于使用嵌入式 Web 服务器运行的应用程序,此条件将不匹配。

SpEL Expression Conditions

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

在表达式中引用一个 Bean 会导致该 Bean 在上下文刷新处理的早期就被初始化。因此,Bean 无法进行后处理(如配置属性绑定),其状态也可能不完整。

测试你的自动配置

自动配置会受到许多因素的影响:用户配置(@Bean 定义和Environment定制)、条件评估(特定库的存在)等。具体来说,每个测试都应创建一个定义明确的 ApplicationContext,它代表了这些自定义的组合。ApplicationContextRunner 提供了实现这一目标的绝佳方法。

在本地镜像中运行测试时,ApplicationContextRunner 无法工作。

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

private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));

如果需要定义多个自动配置,则无需对其声明进行排序,因为它们的调用顺序与运行应用程序时完全相同。

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

@Test
void defaultServiceBacksOff() {
    this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class));
    });
}

@Configuration(proxyBeanMethods = false)
static class UserConfiguration {

    @Bean
    MyService myCustomService() {
        return new MyService("mine");
    }

}

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

@Test
void serviceNameCanBeConfigured() {
    this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
    });
}

运行程序还可用于显示 ConditionEvaluationReport。报告可在 INFODEBUG 级别打印。下面的示例展示了如何使用 ConditionEvaluationReportLoggingListener 在自动配置测试中打印报告。

import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class MyConditionEvaluationReportingTests {

    @Test
    void autoConfigTest() {
        new ApplicationContextRunner()
            .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
            .run((context) -> {
                // Test something...
            });
    }

}

模拟Web Context

如果需要测试仅在 servlet 或反应式网络应用上下文中运行的自动配置,请分别使用 WebApplicationContextRunnerReactiveWebApplicationContextRunner

覆盖 Classpath

还可以测试运行时不存在特定类 和/或 包时的情况。Spring Boot 随附的过滤类加载器(FilteredClassLoader)可方便运行程序使用。在下面的示例中,我们断言如果 MyService 不存在,自动配置将被正确禁用:

@Test
void serviceIsIgnoredIfLibraryIsNotPresent() {
    this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
        .run((context) -> assertThat(context).doesNotHaveBean("myService"));
}

创建你自己的 Starter

一个典型的 Spring Boot 启动程序包含自动配置和定制特定技术基础架构的代码,我们称之为 “acme”。为了使其易于扩展,可以向环境公开专用命名空间中的大量配置键。最后,我们还提供了一个单一的 “starter” 依赖项,以帮助用户轻松上手。
具体来说,自定义Starter可以包含以下内容:

  • 包含 "acme "自动配置代码的autoconfigure模块。
  • starter模块,提供对autoconfigure模块、"acme "和任何通常有用的附加依赖关系的依赖。一言以蔽之,添加starter就能提供开始使用该库所需的一切。

这种将两个模块分开的做法完全没有必要。如果 "acme "有多种口味、选项或可选功能,那么最好将自动配置分开,因为这样可以清楚地表达某些功能是可选的。此外,您还可以制作一个starter,对这些可选的依赖性提出自己的看法。与此同时,其他人也可以只依赖自动配置模块,自己制作具有不同观点的启动器。
如果自动配置比较简单,而且没有可选功能,那么合并starter中的两个模块无疑是一种选择。

命名

您应确保为starter提供正确的命名空间。即使使用不同的 Maven groupId,也不要以 spring-boot 作为模块名的开头。我们可能会在未来为您的自动配置提供官方支持。
根据经验,你应该用starter的名字来命名组合模块。例如,假设你正在为 "acme "创建一个starter,并将自动配置模块命名为 acme-spring-boot,将启动器命名为 acme-spring-boot-starter。如果只有一个模块将两者结合在一起,则将其命名为 acme-spring-boot-starter

配置 keys

如果您的starter提供配置键,请为它们使用唯一的命名空间。特别是,不要在 Spring Boot 使用的命名空间(如 servermanagementspring 等)中包含您的键。如果您使用相同的命名空间,我们将来可能会修改这些命名空间,从而破坏您的模块。根据经验,请在所有键的前缀加上您拥有的命名空间(例如 acme)。
为每个属性添加 javadoc 字段,确保配置键都有文档记录,如下例所示:

@ConfigurationProperties("acme")
public class AcmeProperties {

    /**
     * Whether to check the location of acme resources.
     */
    private boolean checkLocation = true;

    /**
     * Timeout for establishing a connection to the acme server.
     */
    private Duration loginTimeout = Duration.ofSeconds(3);

    // getters/setters ...

}

@ConfigurationProperties 字段 Javadoc 中只能使用纯文本,因为它们在添加到 JSON 之前不会被处理。

以下是我们内部遵循的一些规则,以确保描述的一致性:

  • 不要以 "The "或 "A "作为描述的开头。
  • 对于boolean类型,以 “是否” 或 “启用” 开始描述。
  • 对于基于集合的类型,以 "逗号分隔列表 "开始描述
  • 使用 java.time.Duration 而不是 long,如果默认单位与毫秒不同,请对其进行说明,如 “如果未指定持续时间后缀,将使用秒”。
  • 除非必须在运行时确定,否则不要在说明中提供默认值。

确保触发元数据生成,以便 IDE 也能为你的keys提供帮助。你可能需要查看生成的元数据(META-INF/spring-configuration-metadata.json),以确保你的keys得到了正确的记录。在兼容的集成开发环境中使用自己的starter也是验证元数据质量的一个好主意。

autoconfigure 模块

自动配置模块包含了开始使用该库所需的一切内容。它还可能包含配置键定义(如 @ConfigurationProperties)和任何可用于进一步自定义组件初始化方式的回调接口。

您应该将对该库的依赖标记为可选,这样您就可以更轻松地在项目中包含自动配置模块。如果这样做,就不会提供该库,默认情况下,Spring Boot 会退出。

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure-processor</artifactId>
  <optional>true</optional>
</dependency>

如果您直接在应用程序中定义了自动配置,请确保配置了 spring-boot-maven-plugin 以防止repackage任务将依赖关系添加到 uber jar 中:

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-autoconfigure-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

在 Gradle 中,应在 annotationProcessor 配置中声明依赖关系,如下例所示:

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

Starter 模块

starter实际上是一个空 jar。它的唯一目的是提供使用库所需的依赖项。你可以把它看作是对开始工作所需内容的一种观点。
不要对添加starter的项目作出假设。如果您要自动配置的库通常需要其他starter,请一并提及。如果可选依赖项的数量较多,提供一组适当的默认依赖项可能会比较困难,因为您应避免包含对于库的典型用法而言不必要的依赖项。换句话说,不应包含可选依赖项。

无论采用哪种方式,您的starter都必须直接或间接引用 Spring Boot 核心启动器 (spring-boot-starter)(如果您的starter依赖于其他starter,则无需添加)。如果仅使用自定义启动器创建项目,Spring Boot 的核心功能将因核心启动器的存在而得到尊重。