创建自己的自动配置
如果你在一家开发共享库的公司工作,或者如果你在开源或商业库上工作,你可能想要开发自己的自动配置。 自动配置类可以打包在外部 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
你可以使用 # 字符在导入文件中添加注释。
|
在自动配置类不是顶级类的不常见情况下,其类名应使用 $ 将其与包含类分开,例如 com.example.Outer$NestedAutoConfiguration 。
|
自动配置必须_仅_通过在导入文件中命名来加载。
确保它们在特定的包空间中定义,并且永远不会成为组件扫描的目标。
此外,自动配置类不应启用组件扫描来查找其他组件。
应该使用特定的 @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
和 @ConditionalOnMissingClass
注解允许 @Configuration
类基于特定类的存在或不存在而包含。
由于注解元数据是使用 ASM 解析的,你可以使用 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.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
按名称指定 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.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。
你需要非常小心 bean 定义的添加顺序,因为这些条件是基于到目前为止已处理的内容进行评估的。
因此,我们建议仅在自动配置类上使用 @ConditionalOnBean 和 @ConditionalOnMissingBean 注解(因为这些保证在添加任何用户定义的 bean 定义后加载)。
|
@ConditionalOnBean 和 @ConditionalOnMissingBean 不会阻止 @Configuration 类的创建。
在类级别使用这些条件与用注解标记每个包含的 @Bean 方法之间的唯一区别是,如果条件不匹配,前者会阻止注册 @Configuration 类作为 bean。
|
属性条件
@ConditionalOnProperty
注解允许基于 Spring Environment 属性包含配置。
使用 prefix
和 name
属性指定应该检查的属性。
默认情况下,任何存在且不等于 false
的属性都会匹配。
你还可以使用 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
注解允许根据应用程序是否为部署到 servlet 容器的传统 WAR 应用程序来包含配置。
对于使用嵌入式 web 服务器运行的应用程序,此条件将不匹配。
SpEL 表达式条件
@ConditionalOnExpression
注解允许基于 SpEL 表达式的结果包含配置。
在表达式中引用 bean 将导致该 bean 在上下文刷新处理的早期被初始化。 因此,该 bean 将不符合后处理(如配置属性绑定)的条件,其状态可能不完整。 |
测试你的自动配置
自动配置可能受到许多因素的影响:用户配置(@Bean
定义和 Environment
自定义)、条件评估(特定库的存在)等。
具体来说,每个测试都应该创建一个明确定义的 ApplicationContext
,它代表这些自定义的组合。
ApplicationContextRunner
提供了一种很好的方式来实现这一点。
在原生镜像中运行测试时,ApplicationContextRunner 不起作用。
|
ApplicationContextRunner
通常定义为测试类的字段,以收集基本的、通用的配置。
以下示例确保始终调用 MyServiceAutoConfiguration
:
-
Java
-
Kotlin
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
val contextRunner = ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration::class.java))
如果需要定义多个自动配置,不需要对它们的声明进行排序,因为它们会按照与运行应用程序时完全相同的顺序调用。 |
每个测试都可以使用运行器来表示特定的用例。
例如,下面的示例调用用户配置(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")
}
}
运行器还可以用于显示 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"有多个风格、选项或可选功能,那么最好分离自动配置,因为你可以清楚地表达某些功能是可选的。
此外,你可以创建一个提供这些可选依赖的意见的 starter。
同时,其他人可以仅依赖 autoconfigure
模块,并创建具有不同意见的自己的 starter。
如果自动配置相对简单且没有可选功能,将两个模块合并到 starter 中绝对是一个选项。
命名
你应该确保为你的 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 之前不会被处理。
|
如果你使用 @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 的存在而得到尊重。
|