文章目錄
- 前言
- 自動裝配規則
-
- 類命名規則
- package命名規則
- jar包建構規則
-
- jar包結構
- jar包取名
- 建構自定義的自動裝配
- @ConditionOnBean失效問題
-
- 為何非自動裝配的配置會失效?
- 為何自動裝配的配置就有效?
前言
參考書籍《SpringBoot程式設計思想》— 小馬哥mercyblitz
此書是難得的講述SpringBoot的一本好書,由Spring的注解發展史介紹到Spring的注解驅動,以一個合适的切入點展開對SpringBoot注解驅動的加載和SpringApplication的啟動過程的讨論。
建議有Spring基礎再去看此書,收益頗豐。
本篇文章是上一篇文章 SpringBoot自動裝配魔法之源碼解析 的番外篇,主要的議題有下面兩點:
- 示範一個專業的自動裝配starter應該是怎樣的,以及如何進行自定義的自動裝配
- @ConditionalOnBean注解失效問題
自動裝配規則
類命名規則
從spring.factories檔案中,以EnableAutoConfiguration為key來搜尋

可以發現一個規律,其自動裝配的Bean的名稱都是以AutoConfiguration結尾的,是以這裡我們可以知道,類名需要以AutoConfiguration結尾。
package命名規則
還是以上述的類作為例子,我們随機截取三個類的包名作為示範:
- org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
- org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
可以發現,他們都是以org.springframework.boot.autoconfigure為開頭的,org.springframework.boot說明這些都是官方的自動裝配,而autoconfigure包說明用來存放自動裝配類的。
從這裡我們可以發現,命名的規則就是
${com.xxx.xxx}.autoconfigure.${功能子產品名,如aop}.*AutoConfiguration
jar包建構規則
jar包結構
在官方文檔中建議分為兩個jar包,一個autoconfigure包,存放自動裝配類和spring.factories,一個starter包,用來maven依賴剛剛的autoconfigure。就像下面這樣
而starter單獨一個jar包,依賴于上面的包。
在官方文檔中說到,建議這樣做,但如果需要簡單的話,合并成一個jar包也是可以的。
jar包取名
接下來就是給jar包取名字了,在官方文檔中,推薦開發人員使用如下命名
${module}-spring-boot-starter
此模式屬于“第三方自定義starter”,而官方stater是什麼樣子呢?
spring-boot-starter-${module}
差別就在子產品名在前在後,starter在前則表示此starter為官方定義的。從上面圖檔也可以看出這一點。
建構自定義的自動裝配
接下來就開始自定義一個自動裝配jar了。首先建構一個工程,其工程名為
然後建立一個合适的包名
建構一個自動裝配的配置類
@Configuration
public class StringBeanAutoConfiguration {
@Bean
public String stringBean(){
return "world,hello";
}
}
将以上配置類放入META-INF下的spring.factories檔案中去
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.microservice.original.autoconfigure.springbean.StringBeanAutoConfiguration
這樣一個stater就做好了。然後将其jar依賴添加到另一個工程的pom檔案中去
<dependency>
<groupId>com.microservice.original</groupId>
<artifactId>stringbean-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
測試的工程基本沒東西
編寫引導類
@EnableAutoConfiguration
public class TestAutoConfigure {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
// 非WEB
.web(WebApplicationType.NONE)
.run(args);
// 擷取上下文中,名為stringBean的Bean,類類型為String
String stringBean = context.getBean("stringBean", String.class);
System.out.println(stringBean);
context.close();
}
}
控制台列印
這樣,一個自定義的自動裝配就完成了。
但其實到這裡還不夠專業,你還需要例如條件前置過濾,分析在什麼時候自動裝配,在什麼時候不自動裝配,并不是引入jar包就自動裝配上去。關于條件的配置可以配置在spring-autoconfigure-metadata.properties檔案中。關于前置filter過濾在講解自動裝配的魔法的那篇文章有深入源碼分析的過程。
條件前置過濾其實也隻是粗略過濾一下,實質上詳細的過濾,你需要在自動裝配的配置Bean中打上各種條件過濾注解,例如:
- @ConditionalOnBean:當存在某Bean時進行裝配Bean
- @ConditionalOnClass:當存在某Class時進行裝配Bean
- 以上注解均存在相反注解,例如@ConditionalOnMissingClass:當不存在某Class時進行裝配Bean
- 還有很多@Conditionalxxx注解,當然你也可以自定義
總之,一個合格的條件過濾,是一個專業的自動裝配Bean必不可少的。
@ConditionOnBean失效問題
首先,我們先來看一個示例。自定義一個配置類在包下
@Configuration
public class TestConfiguration {
@Bean
@ConditionalOnBean(User.class)
public Test test() {
return new Test();
}
}
其含義是,在上下文中若有User這個對象的Bean,則裝配Test對象,然後在引導類配置User這個Bean
@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
// 非WEB
.web(WebApplicationType.NONE)
.run(args);
System.out.println("是否有名為user這個Bean: " + context.containsBean("user"));
System.out.println("其類型為: " + context.getBean("user"));
System.out.println("是否有名為test這個Bean: " + context.containsBean("test"));
context.close();
}
@Bean
public User user(){
return new User("xx");
}
}
控制台列印
這裡不禁發起疑問,為什麼明明有User這個Bean,Test卻沒有被裝配進來呢?我們這裡注釋掉Test的Conditional注解
@Bean
//@ConditionalOnBean(User.class)
public Test test() {
return new Test();
}
再次運作,檢視控制台
這個配置類确實有作用,是以問題就出在@ConditionalOnBean注解上。
其實,@ConditionalOnBean這個注解是給自動裝配的配置類使用的,而不是自定義的配置類。
由于此注解的特殊性,其檢查的是上下文中的Bean,而這就依賴于Bean的注冊順序。如果檢查時機過早,導緻了檢查的時候,你需要判斷的Bean都還沒注冊到Spring上下文中,這就失去了此注解需要有的意思。
如果我們将@ConditionalOnBean判斷移到自動裝配的配置Bean上呢?
将我們的自動裝配Bean調整如下
@Configuration
public class StringBeanAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "user")
public String stringBean(){
return "world,hello";
}
}
當上下文中不存在名為user的Bean時才進行裝配。然後引導類如下
@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
// 非WEB
.web(WebApplicationType.NONE)
.run(args);
System.out.println("是否有名為user這個Bean: " + context.containsBean("user"));
System.out.println("是否有名為stringBean這個Bean: " + context.containsBean("stringBean"));
context.close();
}
@Bean
public User user(){
return new User("xx");
}
}
此時是有user這個Bean的,控制台列印
如果把user這個Bean注釋掉呢?
此時的結果是符合預期的。
為何非自動裝配的配置會失效?
因為在解析配置的時候是有一個順序的,若閱讀過源碼就可以知道,掃描到的Bean的順序會比較提前一點處理,假設我這邊有一個引導類,一個配置類
而注冊到IOC容器中的順序如下
此時處理代碼坐标為ConfigurationClassProcessor處理類的processConfigBeanDefinitions方法中
// 這裡parser.getConfigurationClasses()得到的集合就是上述圖檔那個集合
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
// 注冊解析到的類
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
也就是說,注冊順序就是上述那個順序,我們定義的配置類将首先注冊到Spring上下文,其定義的@ConditionalOnBean注解的屬性值此時是第二位解析的(在引導類中),是以此時的Conditional條件就不比對了,因為你的條件Bean都還沒注冊到上下文呢。為了驗證這個想法,我們将conditional注解移到引導類上,引導類是比配置類晚注冊的,照理來說它的條件是可以比對到的。
@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
// 非WEB
.web(WebApplicationType.NONE)
.run(args);
System.out.println("是否有名為user這個Bean: " + context.containsBean("user"));
System.out.println("是否有名為test這個Bean: " + context.containsBean("test"));
context.close();
}
@Bean
@ConditionalOnBean(Test.class)
public User user(){
return new User("xx");
}
}
而我們的配置類如下
@Configuration
public class TestConfiguration {
@Bean
public Test test() {
return new Test();
}
}
運作引導類,控制台列印
結果符合預期,@ConditionalOnBean沒有失效了。
這裡我舉這個例子是為了說明配置順序決定了是否失效,并不是在提供一個解決方案。大家在平時的配置類中最好不要用@ConditionalOnBean注解,此注解是給自動裝配的情況用是比較合适的。因為在平時的配置類中,順序是不能确定的,此順序還依賴掃描的順序,檔案存放的順序,加載方式的順序,具有很大的不确定性。
為何自動裝配的配置就有效?
回顧一下上面的那個集合的圖,可以看到,所有自動裝配的Bean都是在末尾處,它們的順序是得到保障的,是以@ConditionalOnBean注解可以正常使用。
那麼為什麼自動裝配的Bean一定是在集合的末尾處呢?由 自動裝配的魔法 文章中講解的自動裝配的原理可以得知,其核心是由@Import注解實作的
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
而AutoConfigurationImportSelector這個類結構如下所示
可見,它是一個DeferredImportSelector,延遲性的導入特性,正如講解自動裝配的那篇文章中說到的,其解析處理是比普通的Bean都晚
public void parse(Set<BeanDefinitionHolder> configCandidates) {
// 循環解析普通的Bean
for (BeanDefinitionHolder holder : configCandidates) {
BeanDefinition bd = holder.getBeanDefinition();
parse(bd.getBeanClassName(), holder.getBeanName());
}
// 處理延遲Import的Bean
this.deferredImportSelectorHandler.process();
}
在解析的方法中就可以看出此時機,是最晚處理的,是以其在集合清單中處于末尾位置,在注冊自動裝配的Bean時,判斷Bean是否存在的時候就已經把該注冊的Bean都注冊上了,此時的Bean判斷才是合理的。