建立您自己的自動配置
如果您在開發共享庫的公司工作,或者您正在開發開源或商業庫,您可能希望開發自己的自動配置。自動配置類可以打包在外部 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 註解上的 before、beforeName、after 和 afterName 屬性,或使用專用的 @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。 |
屬性條件
@ConditionalOnProperty 註解允許根據 Spring 環境屬性包含配置。使用 prefix 和 name 屬性指定要檢查的屬性。預設情況下,任何存在且不等於 false 的屬性都將匹配。還有一個專門用於布林屬性的 @ConditionalOnBooleanProperty 註解。透過這兩個註解,您還可以使用 havingValue 和 matchIfMissing 屬性建立更高階的檢查。
如果 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。報告可以以 INFO 或 DEBUG 級別列印。以下示例展示瞭如何在自動配置測試中使用 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 應用程式上下文中執行的自動配置,請分別使用 WebApplicationContextRunner 或 ReactiveWebApplicationContextRunner。
覆蓋類路徑
還可以測試當特定類和/或包在執行時不存在時會發生什麼。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 使用的名稱空間中(例如 server、management、spring 等)。如果您使用相同的名稱空間,我們將來可能會以破壞您模組的方式修改這些名稱空間。通常,請用您自己的名稱空間作為所有鍵的字首(例如 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 的核心功能。 |