建立您自己的自動組態

如果您在開發共用程式庫的公司工作,或者您從事開放原始碼或商業程式庫,您可能想要開發自己的自動組態。自動組態類別可以捆綁在外部 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
您可以使用 # 字元在 imports 檔案中新增註解。
自動組態能透過在 imports 檔案中命名來載入。請確保它們定義在特定的套件空間中,並且永遠不是組件掃描的目標。此外,自動組態類別不應啟用組件掃描來尋找其他組件。應改為使用特定的 @Import 註解。

如果您的組態需要以特定順序應用,您可以使用 @AutoConfiguration 註解或專用的 @AutoConfigureBefore@AutoConfigureAfter 註解上的 beforebeforeNameafterafterName 屬性。例如,如果您提供特定於 Web 的組態,則您的類別可能需要在 WebMvcAutoConfiguration 之後應用。

如果您想對不應彼此有任何直接了解的某些自動組態進行排序,您也可以使用 @AutoConfigureOrder。該註解具有與常規 @Order 註解相同的語義,但為自動組態類別提供了專用的順序。

與標準 @Configuration 類別一樣,應用自動組態類別的順序僅影響定義其 Bean 的順序。這些 Bean 後續建立的順序不受影響,而是由每個 Bean 的依賴關係和任何 @DependsOn 關係決定。

條件註解

您幾乎總是希望在您的自動組態類別中包含一個或多個 @Conditional 註解。@ConditionalOnMissingBean 註解是一個常見的範例,用於允許開發人員在不滿意您的預設值時覆寫自動組態。

Spring Boot 包含許多 @Conditional 註解,您可以透過註解 @Configuration 類別或個別 @Bean 方法,在您自己的程式碼中重複使用這些註解。這些註解包括

類別條件

@ConditionalOnClass@ConditionalOnMissingClass 註解允許根據特定類別的存在或不存在來包含 @Configuration 類別。由於註解元數據是透過使用 ASM 進行解析,因此您可以使用 value 屬性來引用真實類別,即使該類別實際上可能未出現在正在執行的應用程式類別路徑上。如果您希望使用 String 值指定類別名稱,也可以使用 name 屬性。

此機制不以相同的方式應用於 @Bean 方法,在 @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),並檢查自動組態是否正確退讓。調用 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")
			}
	}

建立您自己的起步依賴

典型的 Spring Boot 起步依賴包含程式碼,用於自動組態和自訂給定技術的基礎架構,我們將其稱為 “acme”。為了使其易於擴展,可以在專用命名空間中向環境公開許多組態鍵。最後,提供單個「起步依賴」依賴項,以幫助使用者盡可能輕鬆地開始使用。

具體而言,自訂起步依賴可以包含以下內容

  • autoconfigure 模組,其中包含 “acme” 的自動組態程式碼。

  • starter 模組,提供對 autoconfigure 模組以及 “acme” 和通常有用的任何其他依賴項的依賴。簡而言之,新增起步依賴應提供開始使用該程式庫所需的一切。

兩個模組的分離絕非必要。如果 “acme” 有多種風味、選項或可選功能,那麼最好將自動組態分開,因為您可以清楚地表達某些功能是可選的。此外,您可以製作一個起步依賴,提供有關這些可選依賴項的意見。同時,其他人可以僅依賴 autoconfigure 模組並使用不同的意見製作自己的起步依賴。

如果自動組態相對簡單且沒有可選功能,則將兩個模組合併到起步依賴中絕對是一種選擇。

命名

您應確保為您的起步依賴提供適當的命名空間。即使您使用不同的 Maven groupId,也不要以 spring-boot 開頭命名您的模組。我們將來可能會為您自動組態的事物提供官方支援。

作為經驗法則,您應該以起步依賴之後命名組合模組。例如,假設您要為 “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 與 record 類別一起使用,則應透過類別層級 Javadoc 標籤 @param 提供 record 組件的描述 (record 類別中沒有明確的實例欄位來放置常規欄位層級 Javadocs)。

以下是我們在內部遵循的一些規則,以確保描述一致

  • 不要以 “The” 或 “A” 開頭描述。

  • 對於 boolean 類型,以 “Whether” 或 “Enable” 開頭描述。

  • 對於基於集合的類型,以 “Comma-separated list” 開頭描述

  • 使用 java.time.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 建置時,建議在包含自動組態的模組中新增以下依賴項

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

啟動器模組

啟動器實際上是一個空的容器。它唯一的目的是提供使用該函式庫所需的必要依賴項。您可以將其視為對於開始使用所需內容的一種帶有主觀見解的觀點。

不要對加入啟動器的專案做出假設。如果您的自動配置函式庫通常需要其他啟動器,也請提及它們。如果可選依賴項的數量很高,則可能難以提供一組適當的預設依賴項,因為您應避免包含對於函式庫典型用法而言不必要的依賴項。換句話說,您不應包含可選依賴項。

無論如何,您的啟動器必須直接或間接地參考核心 Spring Boot 啟動器 (spring-boot-starter)(如果您的啟動器依賴於另一個啟動器,則無需添加它)。如果專案僅使用您的自訂啟動器建立,則藉由核心啟動器的存在,Spring Boot 的核心功能將會被實現。