创建您自己的自动配置

如果您在开发共享库的公司工作,或者您正在开发开源或商业库,您可能希望开发自己的自动配置。自动配置类可以打包在外部 jar 中,并且仍然可以被 Spring Boot 识别。

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

理解自动配置的 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 注解上的 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 final 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 final class MyAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	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 环境属性包含配置。使用 prefixname 属性指定要检查的属性。默认情况下,任何存在且不等于 false 的属性都将匹配。还有一个专门用于布尔属性的 @ConditionalOnBooleanProperty 注解。通过这两个注解,您还可以使用 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))
如果需要定义多个自动配置,则无需对其声明进行排序,因为它们以与运行应用程序时完全相同的顺序调用。

每个测试都可以使用运行器来表示一个特定的用例。例如,下面的示例调用用户配置(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。报告可以以 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,运行器可以轻松使用它。在以下示例中,我们断言如果 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")
			}
	}

创建您自己的启动器

典型的 Spring Boot 启动器包含用于自动配置和定制给定技术基础设施的代码,我们称之为“acme”。为了使其易于扩展,可以在专用命名空间中向环境公开许多配置键。最后,提供一个单一的“启动器”依赖项,以帮助用户尽可能轻松地开始使用。

具体来说,自定义启动器可以包含以下内容:

  • 包含“acme”自动配置代码的 autoconfigure 模块。

  • starter 模块,它提供了对 autoconfigure 模块以及 "acme" 和任何通常有用的额外依赖项的依赖。简而言之,添加启动器应该提供开始使用该库所需的一切。

这种两个模块的分离绝非必要。如果“acme”有多种风格、选项或可选功能,那么最好分离自动配置,因为您可以清楚地表达某些功能是可选的。此外,您能够制作一个对这些可选依赖项提供意见的启动器。同时,其他人可以只依赖 autoconfigure 模块,并用不同的意见制作自己的启动器。

如果自动配置相对简单且没有可选功能,则将两个模块合并到启动器中绝对是一个选项。

命名

您应该确保为您的启动器提供适当的命名空间。不要以 spring-boot 开头您的模块名称,即使您使用不同的 Maven groupId。我们将来可能会为您自动配置的东西提供官方支持。

通常,您应该以启动器命名组合模块。例如,假设您正在为“acme”创建一个启动器,并且将自动配置模块命名为 acme-spring-boot,将启动器命名为 acme-spring-boot-starter。如果您只有一个模块将两者合并,则将其命名为 acme-spring-boot-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 与记录类一起使用,则记录组件的描述应通过类级 Javadoc 标签 @param 提供(记录类中没有显式实例字段来放置常规字段级 Javadoc)。

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

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

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

  • 对于基于集合的类型,描述以“逗号分隔列表”开头。

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

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

确保触发元数据生成,以便您的键也能获得 IDE 协助。您可能需要查看生成的元数据(META-INF/spring-configuration-metadata.json),以确保您的键已正确文档化。在兼容的 IDE 中使用您自己的启动器也是验证元数据质量的好方法。

“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"
}

启动器模块

启动器实际上是一个空的 jar。其唯一目的是提供与库一起工作所需的依赖项。您可以将其视为对启动所需内容的有主见的看法。

不要对添加启动器的项目做出假设。如果您正在自动配置的库通常需要其他启动器,请也提及它们。如果可选依赖项的数量很多,提供一组适当的默认依赖项可能会很困难,因为您应该避免包含对于库的典型用法而言不必要的依赖项。换句话说,您不应该包含可选依赖项。

无论哪种方式,您的启动器都必须直接或间接引用核心 Spring Boot 启动器(spring-boot-starter)(如果您的启动器依赖于另一个启动器,则无需添加)。如果一个项目仅使用您的自定义启动器创建,则通过核心启动器的存在将尊重 Spring Boot 的核心功能。
© . This site is unofficial and not affiliated with VMware.