创建您自己的自动配置
如果您在开发共享库的公司工作,或者如果您从事开源或商业库的工作,则可能需要开发自己的自动配置。 自动配置类可以捆绑在外部 jar 中,并且仍然被 Spring Boot 获取。
自动配置可以与提供自动配置代码以及您将与之一起使用的典型库的“Starters”相关联。 我们首先介绍构建自己的自动配置所需了解的内容,然后继续执行创建自定义Starters所需的典型步骤。
了解自动配置的 Bean
实现自动配置的类用@AutoConfiguration
.
此注释本身是用@Configuration
,使自动配置成为标准@Configuration
类。
附加@Conditional
注释用于约束何时应用自动配置。
通常,自动配置类使用@ConditionalOnClass
和@ConditionalOnMissingBean
附注。
这确保了自动配置仅在找到相关类并且您尚未声明自己的类时才适用@Configuration
.
查找自动配置候选者
Spring Boot 检查是否存在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件。
该文件应列出您的配置类,每行一个类名,如以下示例所示:
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
您可以使用该字符向导入文件添加注释。# |
在自动配置类不是顶级类的不寻常情况下,它的类名应该用来将其与包含的类分开,例如$ com.example.Outer$NestedAutoConfiguration . |
自动配置只能通过在导入文件中命名来加载。
确保它们是在特定的包空间中定义的,并且它们永远不是组件扫描的目标。
此外,自动配置类不应启用组件扫描以查找其他组件。
特定@Import 应该使用注释。 |
如果您的配置需要按特定顺序应用,您可以使用before
,beforeName
,after
和afterName
属性@AutoConfiguration
注释或专用@AutoConfigureBefore
和@AutoConfigureAfter
附注。
例如,如果您提供特定于 Web 的配置,则可能需要在WebMvcAutoConfiguration
.
如果您想订购某些不应相互直接了解的自动配置,您还可以使用@AutoConfigureOrder
.
该注解与常规@Order
注释,但为自动配置类提供专用顺序。
与标准一样@Configuration
classes,自动配置类的应用顺序仅影响其 bean 的定义顺序。
随后创建这些 Bean 的顺序不受影响,由每个 Bean 的依赖关系和任何@DependsOn
关系。
弃用和替换自动配置类
您可能需要偶尔弃用自动配置类并提供替代方案。 例如,您可能想要更改自动配置类所在的包名称。
由于自动配置类可以在before
/after
ordering 和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
属性来引用真实的类,即使该类实际上可能不会出现在正在运行的应用程序类路径上。
您还可以使用name
属性,如果您更喜欢使用String
价值。
此机制不以同样的方式应用于@Bean
通常返回类型是条件目标的方法:在方法上的条件应用之前,JVM 将加载类并可能处理方法引用,如果类不存在,这些引用将失败。
为了处理这种情况,请单独的@Configuration
class 可用于隔离条件,如以下示例所示:
-
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 因为在这种情况下引用类不会被处理。 |
豆子条件
这@ConditionalOnBean
和@ConditionalOnMissingBean
注释允许根据特定 Bean 的存在与否包含 Bean。
您可以使用value
属性按类型指定 bean 或name
按名称指定 bean。
这search
属性允许您限制ApplicationContext
搜索 bean 时应考虑的层次结构。
当放置在@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()
}
}
在前面的示例中,someService
如果没有类型的 bean,则将创建 beanSomeService
已包含在ApplicationContext
.
您需要非常小心添加 bean 定义的顺序,因为这些条件是根据到目前为止处理的内容来评估的。因此,我们建议仅使用@ConditionalOnBean 和@ConditionalOnMissingBean 自动配置类上的注释(因为保证在添加任何用户定义的 Bean 定义后加载这些注释)。 |
@ConditionalOnBean 和@ConditionalOnMissingBean 不阻止@Configuration 类。在类级别使用这些条件与标记每个包含@Bean 方法,前者阻止注册@Configuration class 作为 bean 的 be。 |
物业条件
这@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
scope,或者具有ConfigurableWebEnvironment
.
响应式 Web 应用程序是使用ReactiveWebApplicationContext
,或者具有ConfigurableReactiveWebEnvironment
.
这@ConditionalOnWarDeployment
和@ConditionalOnNotWarDeployment
注释允许根据应用程序是否是部署到 Servlet 容器的传统 WAR 应用程序来包含配置。
此条件与使用嵌入式 Web 服务器运行的应用程序不匹配。
SpEL表达条件
这@ConditionalOnExpression
注释允许根据 SpEL 表达式的结果包含配置。
在表达式中引用 Bean 将导致该 Bean 在上下文刷新处理中非常早地初始化。 因此,Bean 将不符合后处理条件(例如配置属性绑定),并且其状态可能不完整。 |
测试您的自动配置
自动配置可能受到许多因素的影响:用户配置 (@Bean
definition 和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")
}
}
创建您自己的Starters
一个典型的 Spring Boot Starters包含用于自动配置和自定义给定技术的基础设施的代码,我们称之为“acme”。 为了使其易于扩展,可以向环境公开专用命名空间中的许多配置键。 最后,提供了一个“入门”依赖项,以帮助用户尽可能轻松地入门。
具体来说,自定义Starters可以包含以下内容:
-
这
autoconfigure
包含“acme”的自动配置代码的模块。 -
这
starter
模块,该模块为autoconfigure
模块以及“acme”和任何其他通常有用的依赖项。 简而言之,添加Starters应该提供开始使用该库所需的一切。
这种分为两个模块的分离绝没有必要。
如果“acme”有多种风格、选项或可选功能,那么最好将自动配置分开,因为您可以清楚地表达某些功能是可选的事实。
此外,您还可以制作一个Starters来提供有关这些可选依赖项的意见。
同时,其他人只能依靠autoconfigure
模块并制作自己的入门,并提出不同的意见。
如果自动配置相对简单且没有可选功能,那么合并Starters中的两个模块绝对是一种选择。
命名
您应该确保为您的Starters提供适当的命名空间。
不要以spring-boot
,即使您使用不同的 MavengroupId
.
我们可能会在未来为您自动配置的内容提供官方支持。
根据经验,您应该以Starters命名组合模块。
例如,假设您正在为“acme”创建一个Starters,并将自动配置模块命名为acme-spring-boot
和Startersacme-spring-boot-starter
.
如果只有一个模块将两者组合在一起,请将其命名为acme-spring-boot-starter
.
配置键
如果您的Starters提供配置键,请为它们使用唯一的命名空间。
特别是,不要在 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 标签提供 Record 组件的描述@param
(记录类中没有显式实例字段来放置常规字段级 Javadoc)。
以下是我们在内部遵循的一些规则,以确保描述一致:
-
不要以“The”或“A”开头描述。
-
为
boolean
类型,以“Frether”或“Enable”开头描述。 -
对于基于集合的类型,以“逗号分隔列表”开头描述
-
用
Duration
而不是long
如果默认单位与毫秒不同,则描述默认单位,例如“如果未指定持续时间后缀,则将使用秒”。 -
不要在描述中提供默认值,除非必须在运行时确定它。
确保触发元数据生成,以便 IDE 帮助也可用于您的密钥。
您可能需要查看生成的元数据 (META-INF/spring-configuration-metadata.json
) 以确保您的密钥已正确记录。
在兼容的 IDE 中使用自己的Starters也是验证元数据质量的好主意。
“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"
}
入门模块
发酵剂真的是一个空罐子。 它的唯一目的是提供使用库所需的依赖项。 您可以将其视为对开始所需内容的固执己见。
不要对添加Starters的项目做出假设。 如果您自动配置的库通常需要其他Starters,请同时提及它们。 如果可选依赖项的数量较多,则提供一组适当的默认依赖项可能会很困难,因为您应该避免包含对库的典型使用不需要的依赖项。 换句话说,您不应包含可选的依赖项。
无论哪种方式,您的Starters都必须引用核心 Spring Boot Starters(spring-boot-starter )直接或间接(如果您的Starters依赖于另一个Starters,则无需添加它)。
如果仅使用自定义Starters创建项目,则 Spring Boot 的核心功能将因核心Starters的存在而得到尊重。 |