跳到主要内容

创建您自己的自动配置

DeepSeek V3 中英对照 Creating Your Own Auto-configuration

如果你在一家开发共享库的公司工作,或者你在开发一个开源或商业库,你可能希望开发自己的自动配置。自动配置类可以打包在外部 jar 中,并且仍然可以被 Spring Boot 自动识别。

自动配置可以与一个“starter”关联,该“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
none
提示

你可以使用 # 字符在导入文件中添加注释。

备注

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

如果你的配置需要按特定顺序应用,你可以在 @AutoConfiguration 注解上使用 beforebeforeNameafterafterName 属性,或者使用专门的 @AutoConfigureBefore@AutoConfigureAfter 注解。例如,如果你提供了 Web 相关的配置,你的类可能需要在 WebMvcAutoConfiguration 之后应用。

如果你想对某些彼此之间不应有直接了解的自动配置进行排序,你也可以使用 @AutoConfigureOrder。该注解与常规的 @Order 注解具有相同的语义,但为自动配置类提供了专用的排序功能。

与标准的 @Configuration 类一样,自动配置类应用的顺序仅影响其 Bean 的定义顺序。这些 Bean 后续的创建顺序不受影响,而是由每个 Bean 的依赖关系以及任何 @DependsOn 关系决定。

弃用和替换自动配置类

你可能需要偶尔弃用自动配置类并提供替代方案。例如,你可能希望更改自动配置类所在的包名。

由于自动配置类可能会在 before/after 排序和 excludes 中被引用,因此你需要添加一个额外的文件来告诉 Spring Boot 如何处理替换。要定义替换,可以创建一个 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.replacements 文件,用于指示旧类与新类之间的链接关系。

例如:

com.mycorp.libx.autoconfigure.LibXAutoConfiguration=com.mycorp.libx.autoconfigure.core.LibXAutoConfiguration
none
备注

AutoConfiguration.imports 文件也应更新为 引用替换类。

条件注解

在自动配置类上,你几乎总是需要包含一个或多个 @Conditional 注解。其中,@ConditionalOnMissingBean 注解是一个常见的例子,它用于允许开发者在不满意默认配置的情况下覆盖自动配置。

Spring Boot 包含了多个 @Conditional 注解,你可以通过在你的代码中标注 @Configuration 类或单个的 @Bean 方法来复用这些注解。这些注解包括:

类条件

@ConditionalOnClass@ConditionalOnMissingClass 注解允许 @Configuration 类根据特定类的存在与否来决定是否被包含。由于注解元数据是通过使用 ASM 进行解析的,因此你可以使用 value 属性来引用实际的类,即使该类可能并未出现在运行时的应用程序类路径中。如果你更倾向于使用 String 值来指定类名,你也可以使用 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();
}

}

}
java
提示

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

Bean 条件

@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();
}

}
java

在前面的示例中,如果 ApplicationContext 中尚未包含 SomeService 类型的 bean,那么 someService bean 将会被创建。

提示

你需要注意 Bean 定义的添加顺序,因为这些条件是基于当前已处理的内容进行判断的。因此,我们建议仅在自动配置类上使用 @ConditionalOnBean@ConditionalOnMissingBean 注解(因为这些注解保证在用户定义的 Bean 定义添加之后加载)。

备注

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

提示

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

属性条件

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

如果在 name 属性中给出了多个名称,那么所有属性都必须通过测试,条件才能匹配。

资源条件

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

Web 应用条件

@ConditionalOnWebApplication@ConditionalOnNotWebApplication 注解允许根据应用程序是否为 Web 应用程序来决定是否包含某些配置。基于 Servlet 的 Web 应用程序是任何使用了 Spring WebApplicationContext、定义了 session 范围或具有 ConfigurableWebEnvironment 的应用程序。而基于响应的 Web 应用程序是任何使用了 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的应用程序。

[@ConditionalOnWarDeployment](https://docs.spring.io/spring-boot/3.4.2/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeployment.html)[@ConditionalOnNotWarDeployment](https://docs.spring.io/spring-boot/3.4.2/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWarDeployment.html) 注解允许根据应用程序是否是一个部署到 Servlet 容器的传统 WAR 应用程序来包含配置。对于使用嵌入式 Web 服务器运行的应用程序,此条件将不匹配。

SpEL 表达式条件

@ConditionalOnExpression 注解允许基于 SpEL 表达式 的结果来包含配置。

备注

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

测试你的自动配置

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

注意

ApplicationContextRunner 在原生镜像中运行测试时不起作用。

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

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

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

每个测试可以使用 runner 来代表一个特定的用例。例如,下面的示例调用了用户配置(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");
}

}
java

也可以轻松自定义 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");
});
}
java

Runner 还可以用于显示 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...
});
}

}
java

模拟 Web 上下文

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

覆盖类路径

也可以在运行时测试当特定类和/或包不存在时会发生什么情况。Spring Boot 附带了一个 FilteredClassLoader,运行器可以轻松使用它。在以下示例中,我们断言如果 MyService 不存在,自动配置将正确禁用:

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

创建你自己的 Starter

一个典型的 Spring Boot starter 包含了用于自动配置和定制特定技术(我们暂且称之为 "acme")基础设施的代码。为了使它易于扩展,可以在专用命名空间中暴露一系列配置键到环境中。最后,提供一个单一的 "starter" 依赖项,以帮助用户尽可能轻松地开始使用。

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

  • autoconfigure 模块,其中包含用于 "acme" 的自动配置代码。

  • starter 模块,它提供了对 autoconfigure 模块以及 "acme" 和任何通常有用的额外依赖项的依赖。简而言之,添加 starter 应该提供开始使用该库所需的一切。

这种分成两个模块的方式并不是必须的。如果 "acme" 有多个变体、选项或可选功能,那么最好将自动配置分离出来,这样你可以清晰地表达某些功能是可选的。此外,你可以创建一个 starter,来提供关于这些可选依赖的建议。同时,其他人可以仅依赖 autoconfigure 模块,并根据不同的建议创建自己的 starter。

如果自动配置相对简单且没有可选功能,那么在 starter 中合并这两个模块绝对是一个可行的选择。

命名

你应该确保为你的启动器提供一个合适的命名空间。即使你使用了不同的 Maven groupId,也不要让你的模块名称以 spring-boot 开头。我们未来可能会为你自动配置的功能提供官方支持。

通常来说,你应该根据启动器的名称来命名组合模块。例如,假设你正在为 "acme" 创建一个启动器,并且你将自动配置模块命名为 acme-spring-boot,将启动器命名为 acme-spring-boot-starter。如果你只有一个模块将两者结合在一起,那么将其命名为 acme-spring-boot-starter

配置键

如果你的 starter 提供了配置键,请为它们使用一个唯一的命名空间。特别是,不要将你的键包含在 Spring Boot 使用的命名空间中(例如 servermanagementspring 等)。如果你使用了相同的命名空间,我们未来可能会以破坏你的模块的方式修改这些命名空间。作为经验法则,请为你所有的键添加一个你拥有的命名空间前缀(例如 acme)。

确保通过为每个属性添加字段 Javadoc 来记录配置键,如下例所示:

import java.time.Duration;

import org.springframework.boot.context.properties.ConfigurationProperties;

@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 ...

public boolean isCheckLocation() {
return this.checkLocation;
}

public void setCheckLocation(boolean checkLocation) {
this.checkLocation = checkLocation;
}

public Duration getLoginTimeout() {
return this.loginTimeout;
}

public void setLoginTimeout(Duration loginTimeout) {
this.loginTimeout = loginTimeout;
}

}
java
备注

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

如果你将 @ConfigurationProperties 与 record 类一起使用,那么 record 组件的描述应该通过类级别的 Javadoc 标签 @param 提供(因为在 record 类中没有显式的实例字段来放置常规的字段级别 Javadoc)。

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

  • 描述不要以 "The" 或 "A" 开头。

  • 对于 boolean 类型,描述以 "Whether" 或 "Enable" 开头。

  • 对于基于集合的类型,描述以 "逗号分隔的列表" 开头。

  • 使用 Duration 而不是 long,并在默认单位与毫秒不同时描述默认单位,例如 "如果未指定持续时间后缀,将使用秒"。

  • 不要在描述中提供默认值,除非必须在运行时确定。

请确保触发元数据生成,以便为你的键提供 IDE 辅助功能。你可能需要检查生成的元数据(META-INF/spring-configuration-metadata.json),以确保你的键已正确记录。在兼容的 IDE 中使用你自己的 starter 也是一个好主意,可以验证元数据的质量。

“autoconfigure” 模块

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

提示

你应该将库的依赖标记为可选,这样可以更轻松地在项目中包含 autoconfigure 模块。如果这样做,库将不会被自动提供,默认情况下,Spring Boot 会回退处理。

Spring Boot 使用一个注解处理器来收集自动配置的条件,并将其存储在一个元数据文件(META-INF/spring-autoconfigure-metadata.properties)中。如果该文件存在,Spring Boot 会利用它来尽早过滤掉不匹配的自动配置,从而提升启动速度。

在使用 Maven 构建时,配置编译器插件(3.12.0 或更高版本)以将 spring-boot-autoconfigure-processor 添加到注解处理器路径中:

<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
xml

使用 Gradle 时,依赖项应声明在 annotationProcessor 配置中,如下例所示:

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

启动器模块

这个 starter 实际上是一个空 jar。它的唯一目的是提供与库一起工作所需的依赖项。你可以将其视为对入门所需内容的一种观点性看法。

不要对你添加的 starter 所加入的项目做出假设。如果你正在自动配置的库通常需要其他 starters,请一并提及它们。如果可选依赖项的数量较多,提供一组合适的默认依赖项可能会很困难,因为你应避免包含在库的典型使用中不必要的依赖项。换句话说,你不应包含可选的依赖项。

备注

无论如何,你的启动器必须直接或间接引用核心的 Spring Boot 启动器(spring-boot-starter)(如果你的启动器依赖于另一个启动器,则无需添加它)。如果项目仅使用你的自定义启动器创建,Spring Boot 的核心功能将通过核心启动器的存在得到支持。