深度剖析Spring Boot自動裝配機制實現原理

跟著Mic學架構發表於2021-11-26

Spring Boot自動裝配

在前面的分析中,Spring Framework一直在致力於解決一個問題,就是如何讓bean的管理變得更簡單,如何讓開發者儘可能的少關注一些基礎化的bean的配置,從而實現自動裝配。所以,所謂的自動裝配,實際上就是如何自動將bean裝載到Ioc容器中來。

實際上在spring 3.x版本中,Enable模組驅動註解的出現,已經有了一定的自動裝配的雛形,而真正能夠實現這一機制,還是在spirng 4.x版本中,conditional條件註解的出現。ok,我們來看一下spring boot的自動裝配是怎麼一回事。

自動裝配的演示

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> 
spring:
    redis:
      host: 127.0.0.1 
      port: 6379
 @Autowired
    private RedisTemplate<String,String>redisTemplate;

按照下面的順序新增starter,然後新增配置,使用RedisTemplate就可以使用了? 那大家想沒想過一個問題,為什麼RedisTemplate可以被直接注入?它是什麼時候加入到Ioc容器的呢? 這就是自動裝配。自動裝配可以使得classpath下依賴的包相關的bean,被自動裝載到Spring Ioc容器中,怎麼做到的呢?

深入分析EnableAutoConfiguration

EnableAutoConfiguration的主要作用其實就是幫助springboot應用把所有符合條件的@Configuration配置都載入到當前SpringBoot建立並使用的IoC容器中。

再回到EnableAutoConfiguration這個註解中,我們發現它的import是這樣

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

但是從EnableAutoCOnfiguration上面的import註解來看,這裡面並不是引入另外一個Configuration。而是一個ImportSelector。這個是什麼東西呢?

AutoConfigurationImportSelector是什麼?

Enable註解不僅僅可以像前面演示的案例一樣很簡單的實現多個Configuration的整合,還可以實現一些複雜的場景,比如可以根據上下文來啟用不同型別的bean,@Import註解可以配置三種不同的class

  1. 第一種就是前面演示過的,基於普通bean或者帶有@Configuration的bean進行諸如
  2. 實現ImportSelector介面進行動態注入

實現ImportBeanDefinitionRegistrar介面進行動態注入

CacheService

public class CacheService {
}

LoggerService

public class LoggerService {
}

EnableDefineService

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented 
@Inherited  --允許被繼承
@Import({GpDefineImportSelector.class})
public @interface EnableDefineService {

    String[] packages() default "";
}

GpDefineImportSelector

public class GpDefineImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //獲得指定註解的詳細資訊。我們可以根據註解中配置的屬性來返回不同的class,
        //從而可以達到動態開啟不同功能的目的
    
annotationMetadata.getAllAnnotationAttributes(EnableDefineService.class.getName(),true)
            .forEach((k,v) -> {
                log.info(annotationMetadata.getClassName());
                log.info("k:{},v:{}",k,String.valueOf(v));
            });
        return new String[]{CacheService.class.getName()};
    }
}

EnableDemoTest

@SpringBootApplication
@EnableDefineService(name = "gupao",value = "gupao")
public class EnableDemoTest {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca=SpringApplication.run(EnableDemoTest.class,args);
        System.out.println(ca.getBean(CacheService.class));
        System.out.println(ca.getBean(LoggerService.class));
    }
}

瞭解了selector的基本原理之後,後續再去分析AutoConfigurationImportSelector的原理就很簡單了,它本質上也是對於bean的動態載入。

@EnableAutoConfiguration註解的實現原理

瞭解了ImportSelector和ImportBeanDefinitionRegistrar後,對於EnableAutoConfiguration的理解就容易一些了

它會通過import匯入第三方提供的bean的配置類:AutoConfigurationImportSelector

@Import(AutoConfigurationImportSelector.class)

從名字來看,可以猜到它是基於ImportSelector來實現基於動態bean的載入功能。之前我們講過Springboot @Enable*註解的工作原理ImportSelector介面selectImports返回的陣列(類的全類名)都會被納入到spring容器中。

那麼可以猜想到這裡的實現原理也一定是一樣的,定位到AutoConfigurationImportSelector這個類中的selectImports方法

selectImports

public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
// 從配置檔案(spring-autoconfigure-metadata.properties)中載入 AutoConfigurationMetadata
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
// 獲取所有候選配置類EnableAutoConfiguration
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
         autoConfigurationMetadata, annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(
      AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
//獲取元註解中的屬性
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
//使用SpringFactoriesLoader 載入classpath路徑下META-INF\spring.factories中,
//key= org.springframework.boot.autoconfigure.EnableAutoConfiguration對應的value
   List<String> configurations = getCandidateConfigurations(annotationMetadata,
         attributes);
//去重
   configurations = removeDuplicates(configurations);
//應用exclusion屬性
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
//過濾,檢查候選配置類上的註解@ConditionalOnClass,如果要求的類不存在,則這個候選類會被過濾不被載入
   configurations = filter(configurations, autoConfigurationMetadata);
   //廣播事件
fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

本質上來說,其實EnableAutoConfiguration會幫助springboot應用把所有符合@Configuration配置都載入到當前SpringBoot建立的IoC容器,而這裡面藉助了Spring框架提供的一個工具類SpringFactoriesLoader的支援。以及用到了Spring提供的條件註解@Conditional,選擇性的針對需要載入的bean進行條件過濾

SpringFactoriesLoader

為了給大家補一下基礎,我在這裡簡單分析一下SpringFactoriesLoader這個工具類的使用。它其實和java中的SPI機制的原理是一樣的,不過它比SPI更好的點在於不會一次性載入所有的類,而是根據key進行載入。

首先,SpringFactoriesLoader的作用是從classpath/META-INF/spring.factories檔案中,根據key來載入對應的類到spring IoC容器中。接下來帶大家實踐一下

建立外部專案jar

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>4.3.13.RELEASE</version>
</dependency>

建立bean以及config

public class GuPaoCore {
    public String study(){
        System.out.println("good good study, day day up");
        return "GuPaoEdu.com";
    }
}
@Configuration
public class GuPaoConfig {
    @Bean
    public GuPaoCore guPaoCore(){
        return new GuPaoCore();
    }
}

建立另外一個工程(spring-boot)

把前面的工程打包成jar,當前專案依賴該jar包

<dependency>
    <groupId>com.gupaoedu.practice</groupId>
    <artifactId>Gupao-Core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

通過下面程式碼獲取依賴包中的屬性

執行結果會報錯,原因是GuPaoCore並沒有被Spring的IoC容器所載入,也就是沒有被EnableAutoConfiguration匯入

@SpringBootApplication
public class SpringBootStudyApplication {
    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext ac=SpringApplication.run(SpringBootStudyApplication.class, args);
        GuPaoCore gpc=ac.getBean(GuPaoCore.class);
        System.out.println(gpc.study());
    }
}

解決方案

在GuPao-Core專案resources下新建資料夾META-INF,在資料夾下面新建spring.factories檔案,檔案中配置,key為自定配置類EnableAutoConfiguration的全路徑,value是配置類的全路徑

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gupaoedu.practice.GuPaoConfig

重新打包,重新執行SpringBootStudyApplication這個類。

可以發現,我們編寫的那個類,就被載入進來了。

@EnableAutoConfiguration註解的實現原理

瞭解了ImportSelector和ImportBeanDefinitionRegistrar後,對於EnableAutoConfiguration的理解就容易一些了

它會通過import匯入第三方提供的bean的配置類:AutoConfigurationImportSelector

@Import(AutoConfigurationImportSelector.class)

從名字來看,可以猜到它是基於ImportSelector來實現基於動態bean的載入功能。之前我們講過Springboot @Enable*註解的工作原理ImportSelector介面selectImports返回的陣列(類的全類名)都會被納入到spring容器中。

那麼可以猜想到這裡的實現原理也一定是一樣的,定位到AutoConfigurationImportSelector這個類中的selectImports方法

selectImports

public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
// 從配置檔案(spring-autoconfigure-metadata.properties)中載入 AutoConfigurationMetadata 
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
// 獲取所有候選配置類EnableAutoConfiguration
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
         autoConfigurationMetadata, annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(
      AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
//獲取元註解中的屬性
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
//使用SpringFactoriesLoader 載入classpath路徑下META-INF\spring.factories中,
//key= org.springframework.boot.autoconfigure.EnableAutoConfiguration對應的value
   List<String> configurations = getCandidateConfigurations(annotationMetadata,
         attributes);
//去重
   configurations = removeDuplicates(configurations);
//應用exclusion屬性
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
//過濾,檢查候選配置類上的註解@ConditionalOnClass,如果要求的類不存在,則這個候選類會被過濾不被載入
   configurations = filter(configurations, autoConfigurationMetadata);
   //廣播事件
fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

本質上來說,其實EnableAutoConfiguration會幫助springboot應用把所有符合@Configuration配置都載入到當前SpringBoot建立的IoC容器,而這裡面藉助了Spring框架提供的一個工具類SpringFactoriesLoader的支援。以及用到了Spring提供的條件註解@Conditional,選擇性的針對需要載入的bean進行條件過濾

版權宣告:本部落格所有文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 Mic帶你學架構
如果本篇文章對您有幫助,還請幫忙點個關注和贊,您的堅持是我不斷創作的動力。歡迎關注「跟著Mic學架構」公眾號公眾號獲取更多技術乾貨!

相關文章