创建自定义自动配置
如果你在公司开发共享库,或参与开源/商业库开发,可能希望开发自己的自动配置。 自动配置类可以打包在外部 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 的创建顺序由依赖关系和 @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
和 @ConditionalOnMissingClass
允许根据特定类的存在与否包含 @Configuration
。
由于注解元数据通过 ASM 解析,即使类不在运行时 classpath,也可用 value
属性引用真实类。
如需用类名字符串,可用 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.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration(proxyBeanMethods = false)
// 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
和 @ConditionalOnMissingBean
允许根据特定 bean 的存在与否包含 bean。
可用 value
属性按类型指定 bean,或用 name
按名称指定。
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.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration(proxyBeanMethods = false)
class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
fun someService(): SomeService {
return SomeService()
}
}
上述例中,若 ApplicationContext
中不存在 SomeService
类型 bean,则会创建 someService
。
需注意 bean 定义的添加顺序,这些条件基于已处理的内容评估。
因此建议仅在自动配置类上用 @ConditionalOnBean 和 @ConditionalOnMissingBean (保证在用户自定义 bean 之后加载)。
|
@ConditionalOnBean 和 @ConditionalOnMissingBean 不会阻止 @Configuration 类被创建。
二者的区别在于,类级条件不匹配时不会注册该配置类。
|
声明 @Bean 方法时,尽量在返回类型中提供详细类型信息。
例如,若 bean 的具体类实现了接口,方法返回类型应为具体类而非接口。
这对 bean 条件尤为重要,因为条件评估只能依赖方法签名中的类型信息。
|
属性条件
@ConditionalOnProperty
允许根据 Spring Environment 属性包含配置。
用 prefix
和 name
属性指定需检查的属性。
默认情况下,属性存在且不为 false
即匹配。
也可用专用的 @ConditionalOnBooleanProperty
注解处理布尔属性。
二者均可通过 havingValue
和 matchIfMissing
实现更复杂的判断。
若 name
属性指定多个名称,所有属性都需通过测试条件才匹配。
资源条件
@ConditionalOnResource
允许仅在特定资源存在时包含配置。
资源可用常规 Spring 约定指定,如:file:/home/user/test.dat
。
Web 应用条件
@ConditionalOnWebApplication
和 @ConditionalOnNotWebApplication
允许根据应用是否为 Web 应用包含配置。
Servlet Web 应用指使用 Spring WebApplicationContext
、定义 session
作用域或有 ConfigurableWebEnvironment
。
响应式 Web 应用指使用 ReactiveWebApplicationContext
或有 ConfigurableReactiveWebEnvironment
。
@ConditionalOnWarDeployment
和 @ConditionalOnNotWarDeployment
允许根据应用是否为传统 WAR 部署包含配置。
该条件不会匹配嵌入式 Web 服务器运行的应用。
SpEL 表达式条件
@ConditionalOnExpression
允许根据 SpEL 表达式 结果包含配置。
在表达式中引用 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
),检查自动配置能否正确回退。
调用 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
。
覆盖 Classpath
还可测试某类/包在运行时缺失的情况。
Spring Boot 提供 FilteredClassLoader
,可被 runner 轻松使用。
如下例断言若无 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
模块。 -
starter
模块依赖autoconfigure
,并引入"acme"及常用依赖。 简言之,添加 starter 即可直接使用该库。
这种两模块分离并非必须。
若"acme"有多种风格、选项或可选特性,建议分离自动配置,便于表达部分特性为可选。
同时可打造带主观推荐的 starter,其他人也可仅依赖 autoconfigure
模块。
如果自动配置相对简单且没有可选特性,在 starter 中合并这两个模块绝对是可行的选择。
命名
你应该为你的 starter 提供一个合适的命名空间。
即使你使用不同的 Maven groupId
,也不要以 spring-boot
开头命名你的模块。
我们将来可能会为你自动配置的内容提供官方支持。
一般来说,你应该根据 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 之前不会被处理。
|
如果你将 @ConfigurationProperties
与 record 类一起使用,则应该通过类级 Javadoc 标签 @param
提供 record 组件的描述(因为 record 类中没有显式的实例字段来放置常规的字段级 Javadoc)。
以下是我们内部遵循的一些规则,以确保描述的一致性:
-
不要以"The"或"A"开头描述。
-
对于
boolean
类型,以"Whether"或"Enable"开头描述。 -
对于基于集合的类型,以"Comma-separated list"开头描述。
-
使用
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
)中收集自动配置的条件。
如果该文件存在,它将被用于急切地过滤不匹配的自动配置,这将提高启动时间。
使用 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>
使用 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 的存在而得到支持。
|