建立你自己的自動配置

如果你在一家開發共享庫的公司工作,或者你在開發一個開源或商業庫,你可能希望開發自己的自動配置。自動配置類可以打包在外部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 的核心功能將得到支援。