创建您自己的自动配置

如果你就职于一家开发共享库的公司,或者你正在开发开源或商业库,那么你可能希望开发自己的自动配置。 自动配置类可以打包在外部 JAR 文件中,并且仍然能够被 Spring Boot 检测到。spring-doc.cadn.net.cn

自动配置可关联到一个“Starters”(starter),该Starters不仅提供自动配置代码,还包含与之配合使用的典型库。 我们首先介绍构建您自己的自动配置所需了解的内容,然后继续介绍创建自定义Starters所需的典型步骤spring-doc.cadn.net.cn

理解自动配置的Bean

实现自动配置的类使用 @AutoConfiguration 进行注解。 该注解本身通过 @Configuration 进行元注解,从而使自动配置类成为标准的 @Configuration 类。 此外,还使用其他 @Conditional 注解来约束自动配置的生效条件。 通常,自动配置类会使用 @ConditionalOnClass@ConditionalOnMissingBean 注解。 这可确保自动配置仅在相关类存在、且您尚未自行声明 @Configuration 时才生效。spring-doc.cadn.net.cn

您可以浏览 spring-boot-autoconfigure 的源代码,以查看 Spring 提供的核心 @AutoConfiguration 类(参见 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件)。 您还可以查看其他模块中的对应文件,以查看它们提供的自动配置。spring-doc.cadn.net.cn

定位自动配置候选类

Spring Boot 会检查您发布的 JAR 文件中是否存在一个 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件。 该文件应按行列出您的配置类,每行一个类名,如下例所示:spring-doc.cadn.net.cn

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
您可以在导入文件中使用 # 字符添加注释。
在极少数情况下,若自动配置类并非顶层类,则其类名应使用 $ 与其外围类分隔,例如 com.example.Outer$NestedAutoConfiguration
自动配置必须通过在导入文件中显式指定其名称的方式进行加载。 请确保它们定义在特定的包空间内,且绝不能成为组件扫描的目标。 此外,自动配置类不应启用组件扫描以查找其他组件。 应改用特定的@Import注解。

如果您的配置需要按特定顺序应用,您可以在 @AutoConfiguration 注解上使用 beforebeforeNameafterafterName 属性,或使用专用的 @AutoConfigureBefore@AutoConfigureAfter 注解。 例如,如果您提供了面向 Web 的特定配置,则您的类可能需要在 WebMvcAutoConfiguration 之后应用。spring-doc.cadn.net.cn

如果您希望对某些自动配置进行排序,且这些自动配置彼此之间不应存在任何直接依赖关系,则还可以使用 @AutoConfigureOrder。 该注解与常规的 @Order 注解语义相同,但为自动配置类提供了专用的执行顺序。spring-doc.cadn.net.cn

与标准的 @Configuration 类一样,自动配置类的应用顺序仅影响其定义的 Bean 的顺序。 而这些 Bean 后续的创建顺序则不受此影响,它由各个 Bean 的依赖关系以及任何 @DependsOn 关系决定。spring-doc.cadn.net.cn

弃用并替换自动配置类

您可能需要偶尔弃用自动配置类,并提供替代方案。 例如,您可能希望更改自动配置类所在的包名称。spring-doc.cadn.net.cn

由于自动配置类可能在 before/after 排序及 excludes 中被引用,因此您需要添加一个额外的文件,以告知 Spring Boot 如何处理替换。 要定义替换关系,请创建一个 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.replacements 文件,指明旧类与新类之间的对应关系。spring-doc.cadn.net.cn

com.mycorp.libx.autoconfigure.LibXAutoConfiguration=com.mycorp.libx.autoconfigure.core.LibXAutoConfiguration
AutoConfiguration.imports 文件也应更新为 引用替换类。

条件注解

您几乎总希望在自动配置类上添加一个或多个 @Conditional 注解。 @ConditionalOnMissingBean 注解是一个常见示例,它允许开发人员在不满意您的默认配置时覆盖自动配置。spring-doc.cadn.net.cn

Spring Boot 包含许多@Conditional注解,您可以通过在@Configuration类或单独的@Bean方法上添加注解,在自己的代码中复用这些注解。 这些注解包括:spring-doc.cadn.net.cn

类条件

@ConditionalOnClass@ConditionalOnMissingClass 注解允许根据特定类的存在或缺失来包含 @Configuration 类。 由于注解元数据是通过 ASM 解析的,因此即使该类实际上并未出现在运行时应用程序的类路径中,您仍可使用 value 属性引用实际的类。 如果您更倾向于使用 String 值来指定类名,则也可使用 name 属性。spring-doc.cadn.net.cn

该机制对@Bean方法的适用方式并不相同,此类方法的返回类型通常是条件的目标:在方法上的条件生效之前,JVM 将已加载该类,并可能已处理了方法引用;若该类不存在,则这些处理操作将失败。spring-doc.cadn.net.cn

要处理此场景,可使用一个独立的 @Configuration 类来隔离该条件,如下例所示:spring-doc.cadn.net.cn

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 final class MyAutoConfiguration {

	// Auto-configured beans ...

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass(SomeService.class)
	static class SomeServiceConfiguration {

		@Bean
		@ConditionalOnMissingBean
		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@ConditionalOnMissingBean 注解允许根据特定 Bean 的存在或不存在来决定是否包含某个 Bean。 您可以使用 value 属性按类型指定 Bean,或使用 name 属性按名称指定 Bean。 search 属性允许您限制在查找 Bean 时应考虑的 ApplicationContext 层级结构。spring-doc.cadn.net.cn

当将其置于 @Bean 方法上时,目标类型默认为该方法的返回类型,如下例所示:spring-doc.cadn.net.cn

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public final class MyAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	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。spring-doc.cadn.net.cn

您需要格外注意 Bean 定义的添加顺序,因为这些条件是根据到目前为止已处理的内容进行评估的。 因此,我们建议仅在自动配置类上使用 @ConditionalOnBean@ConditionalOnMissingBean 注解(因为这些注解可确保在任何用户定义的 Bean 定义添加完毕之后再加载)。
@ConditionalOnBean@ConditionalOnMissingBean 不会阻止创建 @Configuration 类。 在类级别使用这些条件与为每个包含的 @Bean 方法单独添加注解之间的唯一区别在于:前者会在条件不匹配时阻止将 @Configuration 类注册为 Bean。
声明@Bean方法时,请在该方法的返回类型中尽可能提供完整的类型信息。 例如,如果您的 Bean 的具体类实现了某个接口,则该 Bean 方法的返回类型应为具体类,而非接口。 在@Bean方法中尽可能提供完整的类型信息尤为重要,尤其是在使用 Bean 条件(bean conditions)时,因为其求值过程只能依赖于方法签名中可用的类型信息。

属性条件

@ConditionalOnProperty 注解允许根据 Spring 环境(Environment)属性来有条件地包含配置。 使用 prefixname 属性指定需要检查的属性。 默认情况下,只要属性存在且其值不等于 false,即视为匹配。 此外,还有一个专用的 @ConditionalOnBooleanProperty 注解,专用于布尔类型属性。 借助这两个注解,您还可以通过使用 havingValuematchIfMissing 属性实现更复杂的条件检查。spring-doc.cadn.net.cn

如果在 name 属性中指定了多个名称,则所有属性都必须通过测试,条件才能匹配。spring-doc.cadn.net.cn

资源条件

@ConditionalOnResource 注解允许仅在存在特定资源时才包含配置。 资源可通过使用 Spring 的常规约定进行指定,如下例所示:file:/home/user/test.datspring-doc.cadn.net.cn

Web 应用程序条件

@ConditionalOnWebApplication@ConditionalOnNotWebApplication 注解允许根据应用程序是否为 Web 应用程序来有条件地包含配置。 基于 Servlet 的 Web 应用程序是指任何使用 Spring WebApplicationContext、定义了 session 作用域,或具有 ConfigurableWebEnvironment 的应用程序。 响应式 Web 应用程序是指任何使用 ReactiveWebApplicationContext 或具有 ConfigurableReactiveWebEnvironment 的应用程序。spring-doc.cadn.net.cn

@ConditionalOnWarDeployment@ConditionalOnNotWarDeployment 注解允许根据应用程序是否为部署到 Servlet 容器的传统 WAR 应用程序,来有条件地包含配置。 对于使用嵌入式 Web 服务器运行的应用程序,此条件将不匹配。spring-doc.cadn.net.cn

SpEL 表达式条件

@ConditionalOnExpression 注解允许根据 SpEL 表达式 的执行结果来包含配置。spring-doc.cadn.net.cn

在表达式中引用一个 Bean 将导致该 Bean 在上下文刷新处理的非常早期阶段即被初始化。 因此,该 Bean 将无法参与后续处理(例如配置属性绑定),其状态可能不完整。

测试您的自动配置

自动配置可能受多种因素影响:用户配置(@Bean 定义和 Environment 自定义)、条件评估(特定库是否存在)以及其他因素。 具体而言,每个测试都应创建一个明确定义的 ApplicationContext,以表示这些自定义项的组合。 ApplicationContextRunner 提供了一种实现该目标的绝佳方式。spring-doc.cadn.net.cn

ApplicationContextRunner 在原生镜像中运行测试时不起作用。

ApplicationContextRunner 通常被定义为测试类中的一个字段,用于收集基础的、通用的配置。 以下示例确保 MyServiceAutoConfiguration 始终被调用:spring-doc.cadn.net.cn

	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
		.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
	val contextRunner = ApplicationContextRunner()
		.withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration::class.java))
如果需要定义多个自动配置,则无需对它们的声明进行排序,因为这些自动配置的调用顺序与应用程序运行时的顺序完全相同。

每个测试均可使用该运行器来表示特定的使用场景。 例如,以下示例调用了一个用户配置(UserConfiguration),并验证自动配置是否能正确退避。 调用 run 会提供一个可用于 AssertJ 的回调上下文。spring-doc.cadn.net.cn

	@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,如下例所示:spring-doc.cadn.net.cn

	@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 在自动配置测试中打印报告。spring-doc.cadn.net.cn

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 应用程序上下文中运行的自动配置,请分别使用 WebApplicationContextRunnerReactiveWebApplicationContextRunnerspring-doc.cadn.net.cn

覆盖类路径

还可以测试当特定类和/或包在运行时不存在时会发生什么情况。 Spring Boot 提供了一个 FilteredClassLoader,可由测试运行器轻松使用。 在以下示例中,我们断言:如果 MyService 不存在,则自动配置将被正确禁用:spring-doc.cadn.net.cn

	@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 Starter 包含用于自动配置和自定义特定技术基础设施的代码,我们暂且将该技术称为“acme”。 为使其易于扩展,可在专用命名空间中向环境暴露一系列配置项(configuration keys)。 最后,提供一个单独的“starter”依赖项,以帮助用户尽可能轻松地快速上手。spring-doc.cadn.net.cn

具体而言,一个自定义 Starter 可以包含以下内容:spring-doc.cadn.net.cn

  • 包含“acme”自动配置代码以及使用该功能的任何API的acme-spring-boot模块。spring-doc.cadn.net.cn

  • 提供“acme”所需的其他Starters依赖项的acme-spring-boot-starter模块,acme-spring-boot,以及可能还有通常有用的其他依赖项。 简而言之,添加该Starters应该提供使用该库所需的一切。spring-doc.cadn.net.cn

将这两个模块分开在任何情况下都不是必须的。 如果“acme”有多种风格、选项或可选功能,那么将自动配置分开会更好,这样可以明确表达某些功能是可选的。 此外,你还可以创建一个提供对这些可选依赖项意见的Starters。 同时,其他人可以只依赖 acme-spring-boot 并创建自己的Starters,带有不同的意见。spring-doc.cadn.net.cn

如果自动配置相对简单,且不包含可选功能,则将这两个模块合并到Starters(starter)中无疑是一种可行方案。spring-doc.cadn.net.cn

如果 "acme" 有一组基本的依赖项,这些依赖项是工作的必要条件,但你希望表达一种更具有倾向性的观点,那么拥有一个单独的 starter 是一个不错的选择。spring-doc.cadn.net.cn

测试“acme”功能时,你可能需要特定于测试的自动配置。 例如,你可以提供一种机制,将外部依赖项替换为内存中的替代实现。 为此,可遵循相同的原则,创建一个独立的、作用域限定为测试的 starter。spring-doc.cadn.net.cn

命名

您应确保为您的Starters提供恰当的命名空间。 即使您使用了不同的 Maven groupId,模块名称也不应以 spring-boot 开头。 未来我们可能会为您的自动配置功能提供官方支持。spring-doc.cadn.net.cn

经验法则:组合模块的名称应以对应的 Starter 命名。 例如,假设您正在为“acme”创建一个 Starter,并将自动配置模块命名为 acme-spring-boot,将 Starter 命名为 acme-spring-boot-starter。 如果仅有一个模块同时包含这两部分,则应将其命名为 acme-spring-boot-starterspring-doc.cadn.net.cn

如果“acme”也有一个测试范围的Starters,则将其命名为 acme-spring-boot-starter-testspring-doc.cadn.net.cn

配置项键名

如果您的Starters提供了配置项,应为其使用唯一的命名空间。 特别注意:请勿将您的配置项置于 Spring Boot 所使用的命名空间下(例如 servermanagementspring 等)。 若您使用了相同的命名空间,我们未来可能会以破坏您模块兼容性的方式修改这些命名空间。 经验法则:请为所有配置项添加您自有命名空间作为前缀(例如 acme)。spring-doc.cadn.net.cn

请确保通过为每个属性添加字段级 Javadoc 来记录配置项的键,如下例所示:spring-doc.cadn.net.cn

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))
您应仅对字段 Javadoc 使用纯文本并设置 `@ConfigurationProperties`,因为这些内容在添加到 JSON 之前不会被处理。

若在记录类(record class)中使用 @ConfigurationProperties,则记录组件的描述应通过类级别的 Javadoc 标签 @param 提供(记录类中不存在显式的实例字段,因此无法在字段级别添加常规的 Javadoc 注释)。spring-doc.cadn.net.cn

以下是我们内部遵循的一些规则,以确保描述保持一致:spring-doc.cadn.net.cn

请务必触发元数据生成,以便您的配置项键(keys)也能获得 IDE 的智能提示支持。 您可能需要检查生成的元数据(META-INF/spring-configuration-metadata.json),以确保您的配置项键已得到恰当的文档说明。 在兼容的 IDE 中使用您自己的 Starter 也是一个不错的做法,可用于验证元数据的质量。spring-doc.cadn.net.cn

“自动配置”模块

autoconfigure 模块包含了使用该库所需的所有内容。 该模块还可能包含配置项定义(例如 @ConfigurationProperties)以及可用于进一步自定义组件初始化方式的任何回调接口。spring-doc.cadn.net.cn

您应将对库的依赖项标记为可选,以便更轻松地在项目中引入 autoconfigure 模块。 采用这种方式时,该库不会被提供,Spring Boot 默认会自动退避。

Spring Boot 使用注解处理器,将自动配置中的条件收集到一个元数据文件(META-INF/spring-autoconfigure-metadata.properties)中。 如果该文件存在,则会利用它提前过滤掉不匹配的自动配置,从而提升启动速度。spring-doc.cadn.net.cn

使用 Maven 构建时,请将编译器插件(3.12.0 或更高版本)配置为在注解处理器路径中添加 spring-boot-autoconfigure-processorspring-doc.cadn.net.cn

<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 配置中声明,如下例所示:spring-doc.cadn.net.cn

dependencies {
	annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}

入门模块

该Starters实际上是一个空的 JAR 文件。 其唯一目的是提供与该库协同工作所必需的依赖项。 您可以将其视为一种“约定优于配置”的视角,即定义了入门所需的各项要求。spring-doc.cadn.net.cn

请勿对引入您 Starter 的项目做任何假设。 如果您的自动配置库通常还需要其他 Starter,请一并提及。 若可选依赖项数量较多,则提供一组恰当的默认依赖项可能较为困难,因为您应避免引入对库的典型使用场景而言非必需的依赖项。 换言之,您不应包含可选依赖项。spring-doc.cadn.net.cn

无论采用哪种方式,您的Starters都必须直接或间接地引用 Spring Boot 核心Starters(spring-boot-starter)(如果您的Starters依赖于其他Starters,则无需显式添加该核心Starters)。 如果项目仅使用您的自定义Starters创建,Spring Boot 的核心功能将因核心Starters的存在而得到支持。