1. 程式人生 > 實用技巧 >Spring Boot 自動裝配原始碼分析

Spring Boot 自動裝配原始碼分析

基於 Spring Boot 開發的應用中,經常需要引用別的 starter 元件或者自定義公司內部使用的 starter。而 starter 的基礎是 Spring Boot的自動裝配。

什麼是自動裝配

自動裝配,簡單說就是自動將 Bean 裝配到 IoC 容器中。下面通過 Spring Boot 整合 Redis 的案例感受一下自動裝配的用法。

  • 專案中引入 starter 依賴:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
  <version>2.1.7.RELEASE</version>
</dependency>
  • 新增配置:在 application.properties 中新增:
spring.redis.host=localhost
spring.redis.port=6379
  • 使用:新建測試類 RedisTestController 中使用 RedisTemplate
@RestController
public class RedisTestController {

    @Autowired
    RedisTemplate<String, String> redisTemplate;
    
    @GetMapping("/hello")
    public String hello() {
        redisTemplate.opsForValue().set("key", "value");
        return "Hello World";
    }
}

這是我們一般的使用方式。在這個案例中我們並沒有通過 xml 或者註解的方式將 RedisTemplate 注入 IoC 容器中,為什麼在 Controller 中可以直接通過 @Autowired 註解注入 RedisTemplate 例項呢?這說明,我們使用該例項之前,容器中已經存在 RedisTemplate 了。這就是 Spring Boot 自動裝配的結果。

自動裝配的實現

@EnableAutoConfiguration

自動裝配在 Spring Boot 中是通過 @EnableAutoConfiguration 註解實現的,這個註解的宣告是在啟動類註解 @SpringBootApplication 內。

@SpringBootApplication
public class TestApplication {

	public static void main(String[] args) {
		SpringApplication.run(TestApplication.class, args);
	}
}

點進 @SpringBootApplication 中:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

可以看到這個是複合註解,點進 @EnableAutoConfiguration 註解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

@EnableAutoConfiguration 中 @AutoConfigurationPackage 的作用是把使用了該註解的類所在的包和子包下所有元件掃描到 Spring IoC 容器中。同時,@Import 匯入了 @AutoConfigurationImportSelector,這個類會匯入需要自動配置的類。

@AutoConfigurationImportSelector

點進 @AutoConfigurationImportSelector 中:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!this.isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  } else {
    // 獲取需要自動配置的類
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
    
    AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
    
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
  }
}

該註解中重寫了頂級介面 ImportSelector 中的 selectImports 方法來匯入自動配置類,其中又呼叫了自身的 getAutoConfigurationEntry 方法:

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
  
  if (!this.isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  } else {
    
    AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
    
    // 獲取所有需要自動配置的候選類
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
    
    // 移除重複的配置項
    configurations = this.removeDuplicates(configurations);
    
    // 排除:根據exclude屬性把不需要自動裝配的配置類排除
    Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
    this.checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = this.filter(configurations, autoConfigurationMetadata);
    
    // 廣播事件
    this.fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
  }
}

ImportSelector 可以實現批量裝配,並且還可以通過邏輯處理來實現 Bean 的選擇性裝配,也就是可以根據上下文來決定哪些類能夠被 IoC 容器初始化。

總的來說,getAutoConfigurationEntry 方法先獲取所有配置類,再通過去重,exclude 排除等操作,得到最終需要自動配置的配置類。這裡著重看如何獲取所有的配置類,也就是 getCandidateConfigurations 方法:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
  
  Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
  
  return configurations;
}

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  return EnableAutoConfiguration.class;
}

protected ClassLoader getBeanClassLoader() {
  return this.beanClassLoader;
}

這裡呼叫 SpringFactoriesLoader.loadFactoryNames 來載入配置,兩個引數,一個是自動配置類的 Class 型別,

EnableAutoConfiguration.class ,另一個是本類的一個類載入器 this.beanClassLoader。

檢視 SpringFactoriesLoader.loadFactoryNames 方法:

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  
  // 獲取Class檔案的全限定名,這裡對於自動配置,即EnableAutoConfiguration.class.getName()
  String factoryTypeName = factoryType.getName();
  
  // 根據下面的方法,通過給定的類載入器載入
  return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
  
  // 先從快取中取
  if (result != null) {
    // 如果快取已經載入過,直接返回結果
    return result;
  } else {
    try {
      // 載入META-INF/spring.factories檔案,獲取URL集合
      Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
      LinkedMultiValueMap result = new LinkedMultiValueMap();

      // 遍歷URL集合
      while(urls.hasMoreElements()) {
        URL url = (URL)urls.nextElement();
        UrlResource resource = new UrlResource(url);
        // 讀取配置
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        Iterator var6 = properties.entrySet().iterator();

        while(var6.hasNext()) {
          Entry<?, ?> entry = (Entry)var6.next();
          String factoryTypeName = ((String)entry.getKey()).trim();
          String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
          int var10 = var9.length;

          for(int var11 = 0; var11 < var10; ++var11) {
            String factoryImplementationName = var9[var11];
            // 結果轉成集合
            result.add(factoryTypeName, factoryImplementationName.trim());
          }
        }
      }

      cache.put(classLoader, result);
      return result;
    } catch (IOException var13) {
      throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
    }
  }
}

這裡用到了 SpringFactoriesLoader ,它是 Spring 內部提供的一種約定俗成的載入方式,類似於Java 中的 SPI。簡單說,它會掃描 classpath 下的 META-INF/spring.factories 檔案,spring.factories 檔案中的資料以 key = value 形式儲存,而 SpringFactoriesLoader.loadFactoryNames 會根據 key 得到對應的 value 值。因此,這個場景中key 對應為 EnableAutoConfiguration ,value 是多個配置類,也就是 getCandidateConfigurations 方法返回值。

spring.factories

找到依賴中的 spring.factories

該配置檔案以鍵值對給出需要自動配置的配置類,通過上述的程式碼獲取 配置類的 String 陣列。

隨便找個值如 org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration 作為示例:

// 配置類
@Configuration(proxyBeanMethods = false)

// 條件判斷,只有指定類載入後,才載入找個配置
@ConditionalOnClass({RabbitTemplate.class, Channel.class})

// 繫結的配置檔案
@EnableConfigurationProperties({RabbitProperties.class})

// 匯入其他配置
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {
  public RabbitAutoConfiguration() {
  }

  @Configuration(
    proxyBeanMethods = false
  )
  @ConditionalOnClass({RabbitMessagingTemplate.class})
  @ConditionalOnMissingBean({RabbitMessagingTemplate.class})
  @Import({RabbitAutoConfiguration.RabbitTemplateConfiguration.class})
  protected static class MessagingTemplateConfiguration {
    protected MessagingTemplateConfiguration() {
    }

    @Bean
    @ConditionalOnSingleCandidate(RabbitTemplate.class)
    public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
      return new RabbitMessagingTemplate(rabbitTemplate);
    }
  }

可以看到這裡對配置做了一定的約束,比如載入順序、載入條件、額外的元件等。

@ConditionalOnClass :該系列註解用於條件判斷,之前寫過關於該註解的筆記。

另外,該配置類還綁定了一個配置檔案 @EnableConfigurationProperties({RabbitProperties.class}) ,可以根據繫結的這個配置檔案做個性化修改:

@ConfigurationProperties(
    prefix = "spring.rabbitmq"
)
public class RabbitProperties {
    private String host = "localhost";
    private int port = 5672;
    private String username = "guest";
    private String password = "guest";
    private final RabbitProperties.Ssl ssl = new RabbitProperties.Ssl();
    private String virtualHost;
    private String addresses;
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration requestedHeartbeat;
    private boolean publisherReturns;
    private ConfirmType publisherConfirmType;
    private Duration connectionTimeout;
    private final RabbitProperties.Cache cache = new RabbitProperties.Cache();
    private final RabbitProperties.Listener listener = new RabbitProperties.Listener();
    private final RabbitProperties.Template template = new Rab

@ConfigurationProperties 標註當前類是配置檔案,它繫結字首為 ”spring.rabbitmq“ 的配置,我們新增到配置會被傳遞到具體的元件覆蓋預設配置。

總結

簡單總結 Spring Boot 的自動裝配流程:

  1. 通過 @EnableAutoConfiguration 註解開啟自動配置。
  2. 自動載入類路徑下 META-INF/spring.factories 檔案,讀取以 EnableAutoConfiguration 的全限定類名對應的值,作為候選配置類。這裡預設給出了很多元件的自動配置類。
  3. 自動配置類可能會再匯入一些依賴(比如 @Import),或者給出一些配置條件,並且會通過 @Bean 註解把該元件所包含的元件注入到 spring 容器中以供使用。
  4. 自動配置類還可能會繫結 xxxProperties 配置檔案類,該類又會和應用程式中的 application.properties 中的指定字首繫結。第 3 步注入元件的時候,元件可能還會獲取配置檔案類中的內容,所以使用者可以在 application.properties 修改指定配置,來制定自己的個性化服務。