创建你自己的自动配置

如果你在一家开发共享库的公司工作,或者你在开发一个开源或商业库,你可能希望开发自己的自动配置。自动配置类可以打包在外部JAR中,并且仍然能被Spring Boot拾取。

自动配置可以与一个“starter”关联,该starter提供自动配置代码以及你通常会使用的典型库。我们首先介绍构建自己的自动配置所需了解的知识,然后转向创建自定义starter所需的典型步骤

理解自动配置的Bean

实现自动配置的类使用@AutoConfiguration注解。此注解本身使用@Configuration进行元注解,使自动配置成为标准的@Configuration类。额外的@Conditional注解用于限制自动配置何时适用。通常,自动配置类使用@ConditionalOnClass@ConditionalOnMissingBean注解。这确保了自动配置仅在找到相关类且你未声明自己的@Configuration时才适用。

定位自动配置候选者

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注解上使用beforebeforeNameafterafterName属性,或者使用专门的@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注册。
在声明@Bean方法时,在方法的返回类型中提供尽可能多的类型信息。例如,如果你的Bean的具体类实现了一个接口,Bean方法的返回类型应该是具体类而不是接口。在使用Bean条件时,在@Bean方法中提供尽可能多的类型信息尤为重要,因为它们的评估只能依赖于方法签名中可用的类型信息。

属性条件

@ConditionalOnProperty注解允许根据Spring Environment属性来包含配置。使用prefixname属性指定应检查的属性。默认情况下,匹配任何存在且不等于false的属性。你还可以使用havingValuematchIfMissing属性创建更高级的检查。

如果在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))
如果需要定义多个自动配置,则无需对其声明进行排序,因为它们在应用运行时会按照完全相同的顺序被调用。

每个测试都可以使用runner来代表一个特定用例。例如,下面的示例调用一个用户配置(UserConfiguration),并检查自动配置是否正确回退(backs off)。调用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。报告可以在INFODEBUG级别打印。以下示例展示了如何在自动配置测试中使用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应用上下文中运行的自动配置,请分别使用WebApplicationContextRunnerReactiveWebApplicationContextRunner

覆盖类路径

也可以测试当特定类和/或包在运行时不存在时会发生什么。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中绝对是一个选项。

命名

你应该确保为你的starter提供一个正确的命名空间。不要以spring-boot开头命名你的模块,即使你使用不同的Maven groupId。我们未来可能会为你的自动配置提供官方支持。

通常情况下,你应以starter命名组合模块。例如,假设你正在为“acme”创建一个starter,并且你将自动配置模块命名为acme-spring-boot,将starter命名为acme-spring-boot-starter。如果只有一个模块包含两者,则将其命名为acme-spring-boot-starter

配置键

如果你的starter提供配置键,请使用唯一的命名空间。特别是,不要将你的键包含在Spring Boot使用的命名空间中(例如servermanagementspring等)。如果你使用相同的命名空间,我们将来可能会以破坏你的模块的方式修改这些命名空间。通常情况下,使用你自己的命名空间作为所有键的前缀(例如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 class一起使用,则record components的描述应通过类级别的Javadoc标签@param提供(record class中没有显式的实例字段来放置常规的字段级别Javadocs)。

以下是我们内部遵循的一些规则,以确保描述一致

  • 不要以“The”或“A”开头描述。

  • 对于boolean类型,以“Whether”或“Enable”开头描述。

  • 对于基于集合的类型,以“Comma-separated list”开头描述

  • 使用Duration而不是long,并描述默认单位(如果与毫秒不同),例如“如果未指定持续时间后缀,将使用秒”。

  • 不要提供默认值描述,除非默认值需要在运行时确定。

确保触发元数据生成,以便你的键也能获得IDE辅助。你可能希望审查生成的元数据(META-INF/spring-configuration-metadata.json),以确保你的键得到妥善文档记录。在兼容的IDE中使用你自己的starter也是验证元数据质量的好方法。

“autoconfigure” 模块

autoconfigure 模块包含启动使用该库所需的一切。它还可以包含配置键定义(例如 @ConfigurationProperties)以及可用于进一步自定义组件如何初始化的任何回调接口。

你应该将对该库的依赖标记为可选(optional),以便更容易地将 autoconfigure 模块包含到你的项目中。如果这样做,该库将不会被提供(provided),默认情况下,Spring Boot 会退避(backs off)。

Spring Boot 使用注解处理器(annotation processor)将自动配置(auto-configurations)的条件收集到一个元数据文件(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 创建,则由于核心 starter 的存在,Spring Boot 的核心功能将得到支持。