天天看點

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

文章目錄

  • 前言
  • 自動裝配規則
    • 類命名規則
    • package命名規則
    • jar包建構規則
      • jar包結構
      • jar包取名
  • 建構自定義的自動裝配
  • @ConditionOnBean失效問題
    • 為何非自動裝配的配置會失效?
    • 為何自動裝配的配置就有效?

前言

參考書籍《SpringBoot程式設計思想》— 小馬哥mercyblitz

此書是難得的講述SpringBoot的一本好書,由Spring的注解發展史介紹到Spring的注解驅動,以一個合适的切入點展開對SpringBoot注解驅動的加載和SpringApplication的啟動過程的讨論。

建議有Spring基礎再去看此書,收益頗豐。

本篇文章是上一篇文章 SpringBoot自動裝配魔法之源碼解析 的番外篇,主要的議題有下面兩點:

  • 示範一個專業的自動裝配starter應該是怎樣的,以及如何進行自定義的自動裝配
  • @ConditionalOnBean注解失效問題

自動裝配規則

類命名規則

從spring.factories檔案中,以EnableAutoConfiguration為key來搜尋

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

可以發現一個規律,其自動裝配的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。就像下面這樣

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

而starter單獨一個jar包,依賴于上面的包。

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

在官方文檔中說到,建議這樣做,但如果需要簡單的話,合并成一個jar包也是可以的。

jar包取名

接下來就是給jar包取名字了,在官方文檔中,推薦開發人員使用如下命名

${module}-spring-boot-starter
           

此模式屬于“第三方自定義starter”,而官方stater是什麼樣子呢?

spring-boot-starter-${module}
           

差別就在子產品名在前在後,starter在前則表示此starter為官方定義的。從上面圖檔也可以看出這一點。

建構自定義的自動裝配

接下來就開始自定義一個自動裝配jar了。首先建構一個工程,其工程名為

然後建立一個合适的包名

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

建構一個自動裝配的配置類

@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>
           

測試的工程基本沒東西

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

編寫引導類

@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();
  }
}
           

控制台列印

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

這樣,一個自定義的自動裝配就完成了。

但其實到這裡還不夠專業,你還需要例如條件前置過濾,分析在什麼時候自動裝配,在什麼時候不自動裝配,并不是引入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");
  }
}
           

控制台列印

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

這裡不禁發起疑問,為什麼明明有User這個Bean,Test卻沒有被裝配進來呢?我們這裡注釋掉Test的Conditional注解

@Bean
//@ConditionalOnBean(User.class)
public Test test() {
  return new Test();
}
           

再次運作,檢視控制台

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

這個配置類确實有作用,是以問題就出在@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的,控制台列印

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

如果把user這個Bean注釋掉呢?

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

此時的結果是符合預期的。

為何非自動裝配的配置會失效?

因為在解析配置的時候是有一個順序的,若閱讀過源碼就可以知道,掃描到的Bean的順序會比較提前一點處理,假設我這邊有一個引導類,一個配置類

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

而注冊到IOC容器中的順序如下

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

此時處理代碼坐标為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();
    }
}
           

運作引導類,控制台列印

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

結果符合預期,@ConditionalOnBean沒有失效了。

這裡我舉這個例子是為了說明配置順序決定了是否失效,并不是在提供一個解決方案。大家在平時的配置類中最好不要用@ConditionalOnBean注解,此注解是給自動裝配的情況用是比較合适的。因為在平時的配置類中,順序是不能确定的,此順序還依賴掃描的順序,檔案存放的順序,加載方式的順序,具有很大的不确定性。

為何自動裝配的配置就有效?

回顧一下上面的那個集合的圖,可以看到,所有自動裝配的Bean都是在末尾處,它們的順序是得到保障的,是以@ConditionalOnBean注解可以正常使用。

那麼為什麼自動裝配的Bean一定是在集合的末尾處呢?由 自動裝配的魔法 文章中講解的自動裝配的原理可以得知,其核心是由@Import注解實作的

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
           

而AutoConfigurationImportSelector這個類結構如下所示

SpringBoot自定義自動裝配與Conditional失效問題前言自動裝配規則建構自定義的自動裝配@ConditionOnBean失效問題

可見,它是一個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判斷才是合理的。

繼續閱讀