Spring Boot 创建你自己的自动配置中文文档
本文为官方文档直译版本。原文链接
Spring Boot 创建你自己的自动配置中文文档
引言
如果您在开发共享库的公司工作,或者如果您在开发开源或商业库,您可能想开发自己的自动配置。自动配置类可以捆绑在外部 jar 中,但仍会被 Spring Boot 接收。
自动配置可以与 “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
您可以使用 # 字符为导入文件添加注释。
自动配置必须通过在导入文件中命名的方式加载。确保它们被定义在特定的包空间中,并且永远不会成为组件扫描的目标。此外,自动配置类不应允许组件扫描查找其他组件。应使用特定的
@Import
注解来代替。
如果需要按特定顺序应用配置,可以使用 @AutoConfiguration 注解或专用的 @AutoConfigureBefore和
@AutoConfigureAfter 注解中的 before
、beforeName
、after
和 afterName
属性。例如,如果您提供特定于 Web 的配置,您的类可能需要在 WebMvcAutoConfiguration
之后应用。
如果您想对某些不应直接相互了解的自动配置进行排序,也可以使用 @AutoConfigureOrder
。该注解与常规的 @Order
注解语义相同,但为自动配置类提供了专用的顺序。
与标准的 @Configuration
类一样,应用自动配置类的顺序只影响定义其 Bean 的顺序。随后创建这些 Bean 的顺序不受影响,而是由每个 Bean 的依赖关系和任何 @DependsOn
关系决定。
条件注释
您几乎总是希望在自动配置类中包含一个或多个 @Conditional
注解。@ConditionalOnMissingBean
注解就是一个常见的例子,它允许开发人员在对默认值不满意时覆盖自动配置。
Spring Boot 包含大量 @Conditional
注解,您可以通过注解 @Configuration
类或单个 @Bean
方法在自己的代码中重复使用这些注解。这些注解包括
- Class Conditions
- Bean Conditions
- Property Conditions
- Resource Conditions
- Web Application Conditions
- SpEL Expression Conditions
Class Conditions
@ConditionalOnClass
和 @ConditionalOnMissingClass
注解使 @Configuration
类可以根据特定类的存在或不存在而被包含。由于注解元数据是通过 ASM 解析的,因此您可以使用 value
属性来引用真正的类,即使该类实际上可能不会出现在运行应用程序的类路径中。如果希望使用字符串值指定类名,也可以使用 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();
}
}
}
如果使用
@ConditionalOnClass
或@ConditionalOnMissingClass
作为元注解的一部分来编写自己的组合注解,则必须使用name
,因为在这种情况下引用类是不会被处理的。
Bean Conditions
@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();
}
}
在前面的示例中,如果 ApplicationContext
中尚未包含 SomeService
类型的 Bean,则将创建 someService
Bean。
您需要非常注意添加 Bean 定义的顺序,因为这些条件是根据目前已处理的内容进行评估的。因此,我们建议在自动配置类中只使用
@ConditionalOnBean
和@ConditionalOnMissingBean
注解(因为这些注解保证在添加任何用户定义的 Bean 定义后加载)。
@ConditionalOnBean
和@ConditionalOnMissingBean
不会阻止@Configuration
类的创建。在类级别使用这些条件与使用注解标记每个包含的@Bean
方法之间的唯一区别是,如果条件不匹配,前者会阻止将@Configuration
类注册为 Bean。
在声明
@Bean
方法时,请在方法的返回类型中提供尽可能多的类型信息。例如,如果 Bean 的具体类实现了一个接口,那么 Bean 方法的返回类型就应该是具体类而不是接口。在@Bean
方法中提供尽可能多的类型信息在使用 Bean 条件时尤为重要,因为对这些条件的评估只能依赖于方法签名中可用的类型信息。
Property Conditions
@ConditionalOnProperty
注解允许根据 Spring 环境属性进行配置。使用prefix
和name
属性指定应检查的属性。默认情况下,任何存在且不等于 false 的属性都会被匹配。您还可以使用 havingValue
和 matchIfMissing
属性创建更高级的检查。
Resource Conditions
@ConditionalOnResource
注解允许配置仅在特定资源存在时才被包含。资源可通过使用通常的 Spring 约定来指定,如下例所示:file:/home/user/test.dat
。
Web Application Conditions
通过 @ConditionalOnWebApplication
和 @ConditionalOnNotWebApplication
注解,可以根据应用程序是否是 Web 应用程序来进行配置。基于 servlet 的 Web 应用程序是指使用 Spring WebApplicationContext
、定义session
作用域或具有 ConfigurableWebEnvironment
的任何应用程序。反应式网络应用是指使用 ReactiveWebApplicationContext
或具有 ConfigurableReactiveWebEnvironment
的任何应用。
通过 @ConditionalOnWarDeployment
和 @ConditionalOnNotWarDeployment
注解,可以根据应用程序是否是部署到 servlet 容器的传统 WAR 应用程序来进行配置。对于使用嵌入式 Web 服务器运行的应用程序,此条件将不匹配。
SpEL Expression Conditions
@ConditionalOnExpression
注解允许根据 SpEL 表达式的结果加入配置。
在表达式中引用一个 Bean 会导致该 Bean 在上下文刷新处理的早期就被初始化。因此,Bean 无法进行后处理(如配置属性绑定),其状态也可能不完整。
测试你的自动配置
自动配置会受到许多因素的影响:用户配置(@Bean
定义和Environment
定制)、条件评估(特定库的存在)等。具体来说,每个测试都应创建一个定义明确的 ApplicationContext
,它代表了这些自定义的组合。ApplicationContextRunner
提供了实现这一目标的绝佳方法。
在本地镜像中运行测试时,
ApplicationContextRunner
无法工作。
ApplicationContextRunner
通常定义为测试类的一个字段,用于收集基本的通用配置。下面的示例确保始终调用 MyServiceAutoConfiguration
:
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
如果需要定义多个自动配置,则无需对其声明进行排序,因为它们的调用顺序与运行应用程序时完全相同。
每个测试都可以使用运行程序来表示特定的用例。例如,下面的示例调用了用户配置(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");
}
}
还可以轻松自定义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");
});
}
运行程序还可用于显示 ConditionEvaluationReport
。报告可在 INFO
或 DEBUG
级别打印。下面的示例展示了如何使用 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...
});
}
}
模拟Web Context
如果需要测试仅在 servlet 或反应式网络应用上下文中运行的自动配置,请分别使用 WebApplicationContextRunner
或 ReactiveWebApplicationContextRunner
。
覆盖 Classpath
还可以测试运行时不存在特定类 和/或 包时的情况。Spring Boot 随附的过滤类加载器(FilteredClassLoader
)可方便运行程序使用。在下面的示例中,我们断言如果 MyService
不存在,自动配置将被正确禁用:
@Test
void serviceIsIgnoredIfLibraryIsNotPresent() {
this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
.run((context) -> assertThat(context).doesNotHaveBean("myService"));
}
创建你自己的 Starter
一个典型的 Spring Boot 启动程序包含自动配置和定制特定技术基础架构的代码,我们称之为 “acme”。为了使其易于扩展,可以向环境公开专用命名空间中的大量配置键。最后,我们还提供了一个单一的 “starter” 依赖项,以帮助用户轻松上手。
具体来说,自定义Starter可以包含以下内容:
- 包含 "acme "自动配置代码的
autoconfigure
模块。 starter
模块,提供对autoconfigure
模块、"acme "和任何通常有用的附加依赖关系的依赖。一言以蔽之,添加starter就能提供开始使用该库所需的一切。
这种将两个模块分开的做法完全没有必要。如果 "acme "有多种口味、选项或可选功能,那么最好将自动配置分开,因为这样可以清楚地表达某些功能是可选的。此外,您还可以制作一个starter,对这些可选的依赖性提出自己的看法。与此同时,其他人也可以只依赖自动配置模块,自己制作具有不同观点的启动器。
如果自动配置比较简单,而且没有可选功能,那么合并starter中的两个模块无疑是一种选择。
命名
您应确保为starter提供正确的命名空间。即使使用不同的 Maven groupId
,也不要以 spring-boot
作为模块名的开头。我们可能会在未来为您的自动配置提供官方支持。
根据经验,你应该用starter的名字来命名组合模块。例如,假设你正在为 "acme "创建一个starter,并将自动配置模块命名为 acme-spring-boot
,将启动器命名为 acme-spring-boot-starter
。如果只有一个模块将两者结合在一起,则将其命名为 acme-spring-boot-starter
。
配置 keys
如果您的starter提供配置键,请为它们使用唯一的命名空间。特别是,不要在 Spring Boot 使用的命名空间(如 server
、management
、spring
等)中包含您的键。如果您使用相同的命名空间,我们将来可能会修改这些命名空间,从而破坏您的模块。根据经验,请在所有键的前缀加上您拥有的命名空间(例如 acme
)。
为每个属性添加 javadoc 字段,确保配置键都有文档记录,如下例所示:
@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 ...
}
在
@ConfigurationProperties
字段 Javadoc 中只能使用纯文本,因为它们在添加到 JSON 之前不会被处理。
以下是我们内部遵循的一些规则,以确保描述的一致性:
- 不要以 "The "或 "A "作为描述的开头。
- 对于
boolean
类型,以 “是否” 或 “启用” 开始描述。 - 对于基于集合的类型,以 "逗号分隔列表 "开始描述
- 使用
java.time.Duration
而不是long
,如果默认单位与毫秒不同,请对其进行说明,如 “如果未指定持续时间后缀,将使用秒”。 - 除非必须在运行时确定,否则不要在说明中提供默认值。
确保触发元数据生成,以便 IDE 也能为你的keys提供帮助。你可能需要查看生成的元数据(META-INF/spring-configuration-metadata.json
),以确保你的keys得到了正确的记录。在兼容的集成开发环境中使用自己的starter也是验证元数据质量的一个好主意。
autoconfigure 模块
自动配置模块包含了开始使用该库所需的一切内容。它还可能包含配置键定义(如 @ConfigurationProperties
)和任何可用于进一步自定义组件初始化方式的回调接口。
您应该将对该库的依赖标记为可选,这样您就可以更轻松地在项目中包含自动配置模块。如果这样做,就不会提供该库,默认情况下,Spring Boot 会退出。
Spring Boot 使用注解处理器在元数据文件(META-INF/spring-autoconfigure-metadata.properties
)中收集自动配置的条件。如果存在该文件,它将用于急切地过滤不匹配的自动配置,从而缩短启动时间。
使用 Maven 构建时,建议在包含自动配置的模块中添加以下依赖关系:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
如果您直接在应用程序中定义了自动配置,请确保配置了 spring-boot-maven-plugin
以防止repackage
任务将依赖关系添加到 uber jar 中:
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
在 Gradle 中,应在 annotationProcessor
配置中声明依赖关系,如下例所示:
dependencies {
annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}
Starter 模块
starter实际上是一个空 jar。它的唯一目的是提供使用库所需的依赖项。你可以把它看作是对开始工作所需内容的一种观点。
不要对添加starter的项目作出假设。如果您要自动配置的库通常需要其他starter,请一并提及。如果可选依赖项的数量较多,提供一组适当的默认依赖项可能会比较困难,因为您应避免包含对于库的典型用法而言不必要的依赖项。换句话说,不应包含可选依赖项。
无论采用哪种方式,您的starter都必须直接或间接引用 Spring Boot 核心启动器 (
spring-boot-starter
)(如果您的starter依赖于其他starter,则无需添加)。如果仅使用自定义启动器创建项目,Spring Boot 的核心功能将因核心启动器的存在而得到尊重。