创建你自己的自动配置
如果你在一家开发共享库的公司工作,或者你正在开发一个开源或商业库,你可能希望开发自己的自动配置。自动配置类可以打包到外部 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
你可以使用 # 字符在 imports 文件中添加注释。
在极少数情况下,如果一个自动配置类不是顶级类,其类名应使用 $ 与其包含类分隔,例如 com.example.Outer$NestedAutoConfiguration。
自动配置类必须仅通过在 imports 文件中命名的方式加载。请确保它们定义在特定的包空间中,并且绝不能成为组件扫描的目标。此外,自动配置类不应启用组件扫描来查找其他组件。应改用特定的 @Import 注解。
如果你的配置需要按特定顺序应用,可以在 @AutoConfiguration 注解上使用 before、beforeName、after 和 afterName 属性,或者使用专门的 @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
AutoConfiguration.imports 文件也应更新为仅引用替换类。
条件注解
你几乎总是希望在你的自动配置类上包含一个或多个 @Conditional 注解。@ConditionalOnMissingBean 注解就是一个常见的例子,用于在开发者对你的默认配置不满意时,允许他们覆盖自动配置。
Spring Boot 包含了许多 @Conditional 注解,你可以通过在自己的 @Configuration 类或单独的 @Bean 方法上添加这些注解来复用它们。这些注解包括:
类条件
[@ConditionalOnClass](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnClass.html) 和 [@ConditionalOnMissingClass](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingClass.html) 注解允许根据特定类的存在或缺失来决定是否包含 [@Configuration](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/context/annotation/Configuration.html) 类。由于注解元数据是通过 ASM 解析的,即使指定的类实际上并未出现在运行时应用程序的 classpath 中,你也可以使用 value 属性引用真实的类。如果你更倾向于使用 String 值来指定类名,也可以使用 name 属性。
该机制并不以相同的方式适用于 @Bean 方法,因为在这些方法中,返回类型通常是条件的目标:在方法上的条件生效之前,JVM 已经加载了该类,并可能已处理了方法引用,而如果该类不存在,则会导致失败。
为处理此场景,可以使用一个单独的 @Configuration 类来隔离该条件,如下例所示:
- Java
- Kotlin
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();
}
}
}
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 ...
class MyAutoConfiguration {
// Auto-configured beans ...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SomeService::class)
class SomeServiceConfiguration {
@Bean
@ConditionalOnMissingBean
fun someService(): SomeService {
return SomeService()
}
}
}
如果你使用 @ConditionalOnClass 或 @ConditionalOnMissingClass 作为元注解的一部分来组合你自己的复合注解,则必须使用 name,因为在这种情况下引用类不会被处理。
Bean 条件
[@ConditionalOnBean](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.html) 和 [@ConditionalOnMissingBean](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.html) 注解允许根据特定 Bean 的存在或缺失来决定是否包含某个 Bean。你可以使用 value 属性按类型指定 Bean,或使用 name 属性按名称指定 Bean。search 属性允许你限制在搜索 Bean 时应考虑的 ApplicationContext 层级范围。
当放置在 @Bean 方法上时,目标类型默认为该方法的返回类型,如下例所示:
- Java
- Kotlin
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();
}
}
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
@AutoConfiguration
class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
fun someService(): SomeService {
return SomeService()
}
}
在前面的示例中,如果 ApplicationContext 中尚未包含类型为 SomeService 的 bean,则会创建 someService bean。
你需要非常小心地注意 Bean 定义的添加顺序,因为这些条件是基于当前已处理的内容进行评估的。因此,我们建议仅在自动配置类上使用 @ConditionalOnBean 和 @ConditionalOnMissingBean 注解(因为这些注解能确保在所有用户定义的 Bean 定义添加之后才加载)。
@ConditionalOnBean 和 @ConditionalOnMissingBean 不会阻止 @Configuration 类被创建。在类级别使用这些条件注解与在每个包含的 @Bean 方法上分别添加注解之间的唯一区别在于:前者在条件不满足时会阻止该 @Configuration 类被注册为 bean。
在声明 @Bean 方法时,应尽可能在方法的返回类型中提供详尽的类型信息。例如,如果你的 bean 的具体类实现了某个接口,则该 bean 方法的返回类型应为具体类,而不是该接口。在使用 bean 条件(bean conditions)时,这一点尤为重要,因为条件的评估只能依赖于方法签名中可用的类型信息。
属性条件
[@ConditionalOnProperty](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.html) 注解允许根据 Spring Environment 中的属性来决定是否包含某个配置。使用 prefix 和 name 属性来指定需要检查的属性。默认情况下,只要属性存在且不等于 false,就视为匹配成功。此外,还有一个专门用于布尔属性的 [@ConditionalOnBooleanProperty](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBooleanProperty.html) 注解。在这两个注解中,你还可以通过 havingValue 和 matchIfMissing 属性来创建更复杂的条件检查。
如果在 name 属性中提供了多个名称,则所有属性都必须通过测试,条件才算匹配。
资源条件
[@ConditionalOnResource](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnResource.html) 注解允许仅在特定资源存在时才包含配置。资源可以使用 Spring 惯用的约定进行指定,如下例所示:file:/home/user/test.dat。
Web 应用条件
[@ConditionalOnWebApplication](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWebApplication.html) 和 [@ConditionalOnNotWebApplication](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWebApplication.html) 注解允许根据应用程序是否为 Web 应用程序来决定是否包含相应的配置。基于 Servlet 的 Web 应用程序是指任何使用 Spring WebApplicationContext、定义了 session 作用域,或具有 ConfigurableWebEnvironment 的应用程序。响应式(Reactive)Web 应用程序是指任何使用 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的应用程序。
[@ConditionalOnWarDeployment](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnWarDeployment.html) 和 [@ConditionalOnNotWarDeployment](https://docs.spring.io/spring-boot/3.5.10/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnNotWarDeployment.html) 注解允许根据应用程序是否为部署到 Servlet 容器的传统 WAR 应用程序来包含配置。对于使用内嵌 Web 服务器运行的应用程序,此条件将不匹配。
SpEL 表达式条件
@ConditionalOnExpression 注解允许根据 SpEL 表达式 的结果来决定是否包含配置。
在表达式中引用一个 bean 会导致该 bean 在上下文刷新处理的非常早期就被初始化。因此,该 bean 将无法参与后处理(例如配置属性绑定),其状态可能是不完整的。
测试你的自动配置
自动配置可能受到多种因素的影响:用户配置(@Bean 定义和 Environment 自定义)、条件评估(特定库的存在)等。具体而言,每个测试都应创建一个明确定义的 ApplicationContext,以代表这些自定义配置的组合。ApplicationContextRunner 提供了一种实现此目标的绝佳方式。
ApplicationContextRunner 在以 native image 方式运行测试时无法正常工作。
ApplicationContextRunner 通常被定义为测试类的一个字段,用于收集基础的、通用的配置。以下示例确保 MyServiceAutoConfiguration 始终被调用:
- Java
- Kotlin
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
val contextRunner = ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration::class.java))
如果需要定义多个自动配置,无需对其声明进行排序,因为它们的调用顺序与应用程序运行时的顺序完全一致。
每个测试都可以使用 runner 来表示一个特定的用例。例如,下面的示例调用了用户配置(UserConfiguration),并检查自动配置是否正确地进行了退避(backs off)。调用 run 会提供一个回调上下文,该上下文可与 AssertJ 一起使用。
- Java
- Kotlin
@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");
}
}
@Test
fun defaultServiceBacksOff() {
contextRunner.withUserConfiguration(UserConfiguration::class.java)
.run { context: AssertableApplicationContext ->
assertThat(context).hasSingleBean(MyService::class.java)
assertThat(context).getBean("myCustomService")
.isSameAs(context.getBean(MyService::class.java))
}
}
@Configuration(proxyBeanMethods = false)
internal class UserConfiguration {
@Bean
fun myCustomService(): MyService {
return MyService("mine")
}
}
也可以轻松地自定义 Environment,如下例所示:
- Java
- Kotlin
@Test
void serviceNameCanBeConfigured() {
this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
assertThat(context).hasSingleBean(MyService.class);
assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
});
}
@Test
fun serviceNameCanBeConfigured() {
contextRunner.withPropertyValues("user.name=test123").run { context: AssertableApplicationContext ->
assertThat(context).hasSingleBean(MyService::class.java)
assertThat(context.getBean(MyService::class.java).name).isEqualTo("test123")
}
}
该 runner 也可用于显示 ConditionEvaluationReport。该报告可以以 INFO 或 DEBUG 级别打印。以下示例展示了如何使用 ConditionEvaluationReportLoggingListener 在自动配置测试中打印该报告。
- Java
- Kotlin
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...
});
}
}
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.assertj.AssertableApplicationContext
import org.springframework.boot.test.context.runner.ApplicationContextRunner
class MyConditionEvaluationReportingTests {
@Test
fun autoConfigTest() {
ApplicationContextRunner()
.withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
.run { context: AssertableApplicationContext? -> }
}
}
模拟 Web 上下文
如果你需要测试一个仅在 Servlet 或响应式 Web 应用程序上下文中运行的自动配置,请分别使用 WebApplicationContextRunner 或 ReactiveWebApplicationContextRunner。
覆盖类路径
也可以测试在运行时某个特定类和/或包不存在时会发生什么情况。Spring Boot 自带了一个 FilteredClassLoader,可被测试运行器轻松使用。在下面的示例中,我们断言:如果 MyService 不存在,则自动配置会被正确禁用:
- Java
- Kotlin
@Test
void serviceIsIgnoredIfLibraryIsNotPresent() {
this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
.run((context) -> assertThat(context).doesNotHaveBean("myService"));
}
@Test
fun serviceIsIgnoredIfLibraryIsNotPresent() {
contextRunner.withClassLoader(FilteredClassLoader(MyService::class.java))
.run { context: AssertableApplicationContext? ->
assertThat(context).doesNotHaveBean("myService")
}
}
创建你自己的 Starter
一个典型的 Spring Boot starter 包含用于自动配置和自定义特定技术(我们称之为 "acme")基础设施的代码。为了使其易于扩展,可以在专用命名空间中向环境暴露若干配置键。最后,提供一个单独的 "starter" 依赖项,以帮助用户尽可能轻松地开始使用。
具体来说,一个自定义 starter 可以包含以下内容:
-
包含 "acme" 的自动配置代码的
autoconfigure模块。 -
提供对
autoconfigure模块以及 "acme" 和其他通常有用的附加依赖项的依赖的starter模块。简而言之,添加该 starter 应该提供开始使用该库所需的一切。
这种拆分为两个模块的做法并非必需。如果 "acme" 有多种变体、选项或可选功能,那么将自动配置(auto-configuration)单独拆分出来会更好,因为这样可以清晰地表明某些功能是可选的。此外,你还可以创建一个 starter,用于表达对这些可选依赖项的特定偏好。同时,其他人也可以仅依赖 autoconfigure 模块,并基于不同的偏好构建自己的 starter。
如果自动配置相对简单,并且没有可选功能,那么将这两个模块合并到 starter 中 definitely 是一个可行的选择。
命名
你应该确保为你的 starter 提供一个合适的命名空间。不要以 spring-boot 开头来命名你的模块,即使你使用了不同的 Maven groupId。我们将来可能会为你自动配置的功能提供官方支持。
通常,你应该以 starter 来命名组合模块。例如,假设你正在为 "acme" 创建一个 starter,并将自动配置模块命名为 acme-spring-boot,starter 命名为 acme-spring-boot-starter。如果你只有一个模块同时包含这两部分,则应将其命名为 acme-spring-boot-starter。
配置键
如果你的 starter 提供了配置键,请为它们使用唯一的命名空间。特别是,不要将你的配置键包含在 Spring Boot 使用的命名空间中(例如 server、management、spring 等)。如果你使用了相同的命名空间,我们将来可能会以破坏你模块的方式修改这些命名空间。一般而言,应将你所有的配置键加上一个你自己拥有的命名空间前缀(例如 acme)。
确保通过为每个属性添加字段 Javadoc 来记录配置键,如下例所示:
- Java
- Kotlin
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;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties
import java.time.Duration
@ConfigurationProperties("acme")
class AcmeProperties(
/**
* Whether to check the location of acme resources.
*/
var isCheckLocation: Boolean = true,
/**
* Timeout for establishing a connection to the acme server.
*/
var loginTimeout:Duration = Duration.ofSeconds(3))
你应该只在 @ConfigurationProperties 字段的 Javadoc 中使用纯文本,因为它们在被添加到 JSON 之前不会被处理。
如果你在 record 类上使用 @ConfigurationProperties,那么 record 组件的描述应通过类级别的 Javadoc 标签 @param 提供(record 类中没有显式的实例字段,无法在其上添加常规的字段级 Javadoc)。
以下是我们内部遵循的一些规则,以确保描述的一致性:
-
描述不要以 “The” 或 “A” 开头。
-
对于
boolean类型,描述以 “Whether” 或 “Enable” 开头。 -
对于基于集合的类型,描述以 “Comma-separated list” 开头。
-
使用 Duration 而非
long,并说明默认单位(如果不同于毫秒),例如 “If a duration suffix is not specified, seconds will be used”。 -
除非默认值需在运行时确定,否则不要在描述中提供默认值。
确保 触发元数据生成,以便你的配置键也能在 IDE 中获得辅助支持。你可能需要检查生成的元数据(META-INF/spring-configuration-metadata.json),以确保你的配置键已正确记录。在兼容的 IDE 中使用你自己编写的 starter 也是验证元数据质量的好方法。
“autoconfigure” 模块
autoconfigure 模块包含了使用该库所需的全部内容。它还可能包含配置键的定义(例如 @ConfigurationProperties)以及可用于进一步自定义组件初始化方式的任何回调接口。
你应该将对库的依赖标记为可选,这样你就可以更轻松地在项目中包含 autoconfigure 模块。如果采用这种方式,该库将不会被提供,默认情况下 Spring Boot 会自动回退。
Spring Boot 使用一个注解处理器来收集自动配置(auto-configurations)上的条件,并将其写入元数据文件(META-INF/spring-autoconfigure-metadata.properties)中。如果该文件存在,它将被用于提前过滤掉不匹配的自动配置,从而提升启动速度。
使用 Maven 构建时,请配置 compiler 插件(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>
在 Gradle 中,依赖项应声明在 annotationProcessor 配置中,如下例所示:
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}
Starter 模块
starter 实际上是一个空的 jar 包。它的唯一目的是提供使用该库所需的依赖项。你可以将其视为一种“约定优于配置”的观点,即明确指出开始使用时所需的内容。
不要对你所添加 starter 的项目做出假设。如果你正在自动配置的库通常需要其他 starter,请一并提及它们。如果可选依赖的数量很多,提供一套合适的默认依赖可能会很困难,因为你应当避免包含在典型使用场景下不必要的依赖。换句话说,你不应包含可选依赖。
无论哪种方式,你的 starter 必须直接或间接引用核心 Spring Boot starter(spring-boot-starter)(如果你的 starter 依赖于另一个 starter,则无需显式添加它)。如果一个项目仅使用你自定义的 starter 创建,Spring Boot 的核心特性将通过核心 starter 的存在而被启用。