# 7.12. 创建您自己的自动配置

如果您在一家开发共享库的公司工作，或者如果您在开源或商业库中工作，您可能想要开发自己的自动配置。自动配置类可以捆绑在外部 jar 中，并且仍然可以被 Spring Boot 拾取。

自动配置可以与提供自动配置代码以及您将使用的典型库的“启动器”相关联。我们首先介绍了构建您自己的自动配置所需了解的内容，然后我们继续介绍[创建自定义启动器所需的典型步骤](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.custom-starter)。 一个[演示项目](https://github.com/snicoll-demos/spring-boot-master-auto-configuration)可用于展示如何逐步创建启动器。

#### 7.12.1.了解自动配置的 Bean

在幕后，自动配置是通过`@AutoConfiguration`注解实现的。这个注解本身是用 `@Configuration`元注解的，可以使自动配置成为标准`@Configuration`类。添加`@Conditional`注解用于限制何时应用自动配置。通常，自动配置类使用`@ConditionalOnClass`和`@ConditionalOnMissingBean`注解。这确保了自动配置仅在找到相关类并且您没有声明自己的类时适用`@Configuration`。

您可以浏览源代码[`spring-boot-autoconfigure`](https://github.com/spring-projects/spring-boot/tree/v2.7.3/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure)以查看`@Configuration`Spring 提供的类（参见[`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`](https://github.com/spring-projects/spring-boot/tree/v2.7.3/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)文件）。

#### 7.12.2. 定位自动配置候选

Spring Boot 检查发布的 jar中是否存在`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`文件。该文件应列出您的配置类，如以下示例所示：

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

您可以通过在此文件中使用`#`注释。 *自动配置只能* 以这种方式加载。确保它们是在特定的包空间中定义的，并且它们永远不是组件扫描的目标。此外，自动配置类不应启用组件扫描以查找其他组件。应该使用 特定的 `@Import`来代替。

如果您的配置需要按特定顺序应用，您可以使用[`@AutoConfigureAfter`](https://github.com/spring-projects/spring-boot/tree/v2.7.3/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureAfter.java)或[`@AutoConfigureBefore`](https://github.com/spring-projects/spring-boot/tree/v2.7.3/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigureBefore.java)注解。例如，如果您提供特定于 Web 的配置，您的`WebMvcAutoConfiguration`类可能需要在.

如果您使用[`@AutoConfiguration`](https://github.com/spring-projects/spring-boot/tree/v2.7.3/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfiguration.java)注解，则可以使用`before`、 `beforeName`、`after`和`afterName`属性别名来代替专用注解。 如果您想订购某些彼此不应该有任何直接了解的自动配置，您也可以使用`@AutoConfigureOrder`. 该注解与常规注解具有相同的语义，但`@Order`为自动配置类提供了专用顺序。

与标准`@Configuration`类一样，应用自动配置类的顺序只影响定义它们的 bean 的顺序。随后创建这些 bean 的顺序不受影响，由每个 bean 的依赖关系和任何`@DependsOn`定义的关系决定。

#### 7.12.3. 条件注解

您几乎总是希望`@Conditional`在您的自动配置类中包含一个或多个注解。注解是一个常见的`@ConditionalOnMissingBean`例子，如果开发人员对你的默认设置不满意，它可以让他们覆盖自动配置。

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

* [类条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.class-conditions)
* [bean条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.bean-conditions)
* [属性条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.property-conditions)
* [资源条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.resource-conditions)
* [网络应用条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.web-application-conditions)
* [SpEL 表达式条件](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations.spel-conditions)

**7.12.3.1.class条件**

`@ConditionalOnClass`和`@ConditionalOnMissingClass`注解允许根据`@Configuration`特定类的存在与否来包含类。由于注解元数据是使用[ASM](https://asm.ow2.io/)解析的，因此您可以使用该`value`属性来引用真实的类，即使该类实际上可能不会出现在正在运行的应用程序类路径中。`name`如果您更喜欢使用值指定类名，也可以使用该属性`String`。

此机制不适用于`@Bean`通常返回类型是条件目标的方法：在方法上的条件适用之前，JVM 将加载类和可能处理的方法引用，如果类不是，则这些方法引用将失败当下。

为了处理这种情况，可以使用一个单独的`@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`在不处理这种情况下使用 as 引用类。

**7.12.3.2. bean条件**

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

放置在`@Bean`方法上时，目标类型默认为方法的返回类型，如下例所示：

```
@AutoConfiguration
public class MyAutoConfiguration {
​
    @Bean
    @ConditionalOnMissingBean
    public SomeService someService() {
        return new SomeService();
    }
​
}
```

在前面的示例中，`someService`如果.`SomeService``ApplicationContext` 您需要非常小心添加 bean 定义的顺序，因为这些条件是根据到目前为止已处理的内容进行评估的。出于这个原因，我们建议仅在自动配置类上使用`@ConditionalOnBean`和`@ConditionalOnMissingBean`注解（因为保证在添加任何用户定义的 bean 定义后加载这些注解）。 `@ConditionalOnBean`并且`@ConditionalOnMissingBean`不要阻止`@Configuration`创建类。在类级别使用这些条件和用`@Bean`注解标记每个包含的方法之间的唯一区别是，如果条件不匹配 ，前者会阻止将类注册为 bean。`@Configuration` 声明`@Bean`方法时，在方法的返回类型中提供尽可能多的类型信息。例如，如果你的 bean 的具体类实现了一个接口，那么 bean 方法的返回类型应该是具体类而不是接口。在使用 bean 条件时，在方法中提供尽可能多的类型信息`@Bean`尤为重要，因为它们的评估只能依赖于方法签名中可用的类型信息。

**7.12.3.3. 属性条件**

`@ConditionalOnProperty`注解允许基于 Spring Environment 属性包含配置。使用`prefix`和`name`属性指定应检查的属性。默认情况下，匹配任何存在但不等于`false`的属性。您还可以使用`havingValue`和`matchIfMissing`属性创建更高级的检查。

**7.12.3.4.资源条件**

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

**7.12.3.5.网络应用条件**

`@ConditionalOnWebApplication`和注解允许根据`@ConditionalOnNotWebApplication`应用程序是否为“Web 应用程序”来包含配置。基于 servlet 的 Web 应用程序是任何使用 Spring `WebApplicationContext`、定义`session`范围或具有`ConfigurableWebEnvironment`. 反应式 Web 应用程序是任何使用`ReactiveWebApplicationContext`或具有`ConfigurableReactiveWebEnvironment`.

注解允许根据`@ConditionalOnWarDeployment`应用程序是否是部署到容器的传统 WAR 应用程序来包含配置。对于使用嵌入式服务器运行的应用程序，此条件将不匹配。

**7.12.3.6.SpEL 表达条件**

`@ConditionalOnExpression`注解允许基于[SpEL 表达式](https://docs.spring.io/spring-framework/docs/5.3.22/reference/html/core.html#expressions)的结果包含配置。

在表达式中引用 bean 将导致该 bean 在上下文刷新处理中很早就被初始化。结果，bean 将不适合进行后处理（例如配置属性绑定），并且其状态可能不完整。

#### 7.12.4.测试您的自动配置

自动配置可能受到许多因素的影响：用户配置（定义`@Bean`和定制`Environment`）、条件评估（特定库的存在）等。具体来说，每个测试都应该创建一个定义良好`ApplicationContext`的，代表这些定制的组合。 `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");
    });
}
```

`Runner`也可用于显示`ConditionEvaluationReport`. 报告可以在`INFO`或`DEBUG`水平打印。以下示例显示了如何使用`ConditionEvaluationReportLoggingListener`打印自动配置测试中的报告。

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

**7.12.4.1.模拟 Web 上下文**

如果您需要测试仅在 servlet 或响应式 Web 应用程序上下文中运行的自动配置，请分别使用`WebApplicationContextRunner`或`ReactiveWebApplicationContextRunner`。

**7.12.4.2. 重写类路径**

还可以测试在运行时不存在特定类和/或包时会发生什么。Spring Boot 附带一个runner 可以轻松使用的`FilteredClassLoader`。在以下示例中，我们断言如果`MyService`不存在，则自动配置被正确禁用：

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

#### 7.12.5.创建自己的启动器(starter)

一个典型的 Spring Boot 启动器包含自动配置和自定义给定技术的基础设施的代码，我们称之为“acme”。为了使其易于扩展，可以将专用命名空间中的许多配置键暴露给环境。最后，提供了一个“starter”依赖项来帮助用户尽可能轻松地开始。

具体来说，自定义启动器可以包含以下内容：

* 包含“acme”的自动配置代码的`autoconfigure`模块。
* 提供对`starter`模块的依赖关系的`autoconfigure`模块以及“acme”和通常有用的任何其他依赖关系。简而言之，添加启动器应该提供开始使用该库所需的一切。

两个模块中的这种分离绝不是必要的。如果“acme”有多种风格、选项或可选功能，那么最好将自动配置分开，因为您可以清楚地表达某些功能是可选的事实。此外，您还可以制作一个启动器来提供有关这些可选依赖项的意见。同时，其他人只能依靠`autoconfigure`模块，制作自己的不同意见的starter。

如果自动配置相对简单并且没有可选功能，那么在启动器中合并两个模块绝对是一种选择。

**7.12.5.1.命名**

您应该确保为您的启动器提供适当的命名空间。即使您使用不同的 Maven `groupId`，也不要使用以`spring-boot`开头的模块名称。以便我们将来可能会为您自动配置的内容提供官方支持。

根据经验，您应该在启动器之后命名组合模块。例如，假设您正在为“acme”创建一个启动器，并且您命名自动配置模块`acme-spring-boot`和启动器`acme-spring-boot-starter`。如果您只有一个模块将两者结合起来，请将其命名为`acme-spring-boot-starter`.

**7.12.5.2. 配置键(ConfigurationProperties)**

如果您的启动器提供配置键，请为它们使用唯一的命名空间。特别是，不要将您的键包含在 Spring Boot 使用的命名空间中（例如`server`、`management`、`spring`等）。如果您使用相同的命名空间，我们将来可能会以破坏您的模块的方式修改这些命名空间。根据经验，在所有键前面加上您自己的命名空间（例如`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`类型，以“Whether”或“Enable”开始描述。
* 对于基于集合的类型，以“Comma-separated list”开始描述
* 如果默认单位与毫秒不同，则使用`java.time.Duration`而不是`long`描述默认单位，例如“If a duration suffix is not specified, seconds will be used”。
* 除非必须在运行时确定，否则不要在描述中提供默认值。

确保[触发元数据生成](https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#appendix.configuration-metadata.annotation-processor)，以便您的密钥也可以使用 IDE 帮助。您可能需要查看生成的元数据 ( `META-INF/spring-configuration-metadata.json`) 以确保正确记录您的密钥。在兼容的 IDE 中使用您自己的启动器也是验证元数据质量的好主意。

**7.12.5.3.“自动配置”模块**

`autoconfigure`模块包含开始使用该库所需的一切。它还可能包含配置键定义（例如`@ConfigurationProperties`）和任何回调接口，可用于进一步自定义组件的初始化方式。

您应该将库的依赖项标记为可选，以便您可以更轻松地将`autoconfigure`模块包含在项目中。如果您这样做，则不会提供该库，并且默认情况下，Spring Boot 会退出。 Spring Boot 使用注解处理器来收集元数据文件 ( `META-INF/spring-autoconfigure-metadata.properties`) 中的自动配置条件。如果该文件存在，它将用于急切地过滤不匹配的自动配置，这将缩短启动时间。建议在包含自动配置的模块中添加以下依赖项：

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

如果您在应用程序中直接定义了自动配置，请确保配置了`spring-boot-maven-plugin`以防止`repackage`目标将依赖项添加到 fat 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 4.5 及更早版本，应在`compileOnly`配置中声明依赖项，如以下示例所示：

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

对于 Gradle 4.6 及更高版本，应在`annotationProcessor`配置中声明依赖项，如下例所示：

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

**7.12.5.4.Starter模块**

启动器实际上是一个空罐子。它的唯一目的是提供必要的依赖项以使用该库。您可以将其视为对入门所需内容的固执己见。

不要对添加启动器的项目做出假设。如果您要自动配置的库通常需要其他启动器，请同时提及它们。如果可选依赖项的数量很高，则提供一组适当的*默认*依赖项可能会很困难，因为您应该避免包含对于库的典型使用而言不必要的依赖项。换句话说，您不应该包含可选依赖项。 无论哪种方式，您的 starter 都必须直接或间接引用核心 Spring Boot starter ( `spring-boot-starter`)（如果您的 starter 依赖于另一个 starter，则无需添加它）。如果仅使用您的自定义启动器创建项目，则 Spring Boot 的核心功能将因核心启动器的存在而受到尊重。
