建立你自己的自動配置
如果你在一家開發共享庫的公司工作,或者你在開發一個開源或商業庫,你可能希望開發自己的自動配置。自動配置類可以打包在外部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
註解上使用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 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註冊。 |
屬性條件
@ConditionalOnProperty
註解允許根據Spring Environment屬性來包含配置。使用prefix
和name
屬性指定應檢查的屬性。預設情況下,匹配任何存在且不等於false
的屬性。你還可以使用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))
如果需要定義多個自動配置,則無需對其宣告進行排序,因為它們在應用執行時會按照完全相同的順序被呼叫。 |
每個測試都可以使用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
。報告可以在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
,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使用的名稱空間中(例如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
與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 的核心功能將得到支援。 |