springboot啓動流程源碼分析(二)

前言:前面和大家一起學習了springboot啓動流程源碼中如何從springboot過度到spring以及springboot如何內置tomcat,如何還不瞭解的童鞋可以去看一下之前的文章(springboot啓動流程源碼分析(1))。今天和大家一起學習的是springboot如何加載第三方的starter,只有熟悉了這個原理我們纔會發現自定義springboot的starter也是非常容易。

一、引入思考的問題

1、springboot未出現之前,我們在在spring項目中如果要使用數據源(比如我們使用druid),需要做哪些事情呢?

(1)引入druid的jar包

(2)配置數據源的參數

(2)在xml文件中配置數據源的bean,或者使用註解的方式@bean注入到spring容器中

2、當我們使用springboot項目時,需要做的事情有哪些?

(1)引入相應的druid的starter的包

(2)在yml或者properties中配置數據源參數

對比上面兩個步驟,我們發現springboot中,我們並沒有顯示的向spring容器中注入相應的datasource的bean,但是我們爲什麼能夠直接使用數據源呢(比如使用事務的時候)

今天咱們要學習的內容,就是解釋下上面的問題,如果你還不瞭解上述的原理,那咱們開始吧。在學習之前如果大家瞭解SPI(service provider interface)那就更好了,因爲這裏面其實就是用到了SPI的機制,SPI引用還是非常廣泛的,比如spring、dubbo中都有廣泛使用

二、springboot啓動加載starter

我們還是從啓動類開始分析

@SpringBootApplication
public class HellobootApplication {

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

}

我們進入@SpingBootApplication的註解

@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 {...}

我們關注這一行註解@Import(AutoConfigurationImportSelector.class)這裏需要spring的知識,如果想要了解這一行註解的原理,可以自行在網上查取(其實原理就是ConfigurationClassPostProcessor這個處理器起的作用),暫時不瞭解也沒關係,在這裏先解釋一下,他會注入AutoConfigurationImportSelector的bean到spring容器,然而這個類又實現了ImportSelector,所以會調用selectImports,並且該方法返回的String[]的內容全部會注入到spring容器中。我們看下這個類的selectImports()方法

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
    //重點看這裏
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
         annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

到了這裏,我們差不多已經見到了勝利的曙光了,springboot啓動流程的原理還是相對比較容易的,當然前提是你需要對spring的知識有一定的瞭解。上述方法很簡單,我們只要關注它需要向spring容器裏面注入哪些bean(不要跑偏了,我們之前帶着問題,就是爲什麼我們沒有顯示向spring容器中注入datasource,但是在springboot中我們能直接拿到),所以我們接着看getAutoConfigurationEntry()會給我們返回哪些string

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
    //拿到所有候選的需要注入到spring容器的bean的全路徑
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    //刪除重複,其實就是用set轉存了一下
   configurations = removeDuplicates(configurations);
    //排除一些我們不需要的...
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
   configurations = filter(configurations, autoConfigurationMetadata);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

上面這個方法也挺好理解的,從每一行代碼的名字,我們大概就知道是做了哪些事情,爲了簡單梳理主要的流程,我這裏只看getCandidateConfigurations(),看看springboot如何幫我們找到需要注入到spring容器中的對象

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         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;
}

你如果留意的話,會看到一個校驗的信息"…META-INF/spring.factories…",暫時先不說,這個方法很簡單,我們直接看第一行代碼跟下一個方法SpringFactoriesLoader.loadFactoryNames()

String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    //先從緩存裏面拿,拿到了直接返回,提高性能,因爲後面的掃描比較耗時
   MultiValueMap<String, String> result = cache.get(classLoader);
   if (result != null) {
      return result;
   }

   try {
       //這裏就是通過類加載器去掃描所有的"META-INF/spring.factories"文件
      Enumeration<URL> urls = (classLoader != null ?
            classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
      result = new LinkedMultiValueMap<>();
       //下面就不用了,掃描了很多spring.factories,需要一個個處理
      while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         UrlResource resource = new UrlResource(url);
         Properties properties = PropertiesLoaderUtils.loadProperties(resource);
         for (Map.Entry<?, ?> entry : properties.entrySet()) {
            String factoryTypeName = ((String) entry.getKey()).trim();
            for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
               result.add(factoryTypeName, factoryImplementationName.trim());
            }
         }
      }
       //加入緩存
      cache.put(classLoader, result);
      return result;
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
            FACTORIES_RESOURCE_LOCATION + "]", ex);
   }
}

這裏我將兩個方法寫一起了,第一個方法太簡單了,我們直接看第二個吧,第二個方法的主要的核心代碼已經註釋了,下面總結一下:

(1)首先從判斷緩存裏面拿,拿不到就掃描

(2)掃描所有jar包中的META-INF/spring.factories文件,並處理裏面的內容(SPI)

(3)加入緩存,方便下次掃描

也就是說,前面跟了那麼多步驟,其實springboot要做的事情就是獲取每個jar包中的META-INF/spring.factories文件,然後將裏面的內容通過反射的方式創建對象,放入到spring容器中管理

那我們看看這些META-INF/spring.factories文件的內容

(1)springboot自帶的

在這裏插入圖片描述

(2)druid-start裏面的
在這裏插入圖片描述

所以通過上面的分析,我們的spring容器會自動註冊"com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"這個bean,看下這個類吧

@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,
    DruidStatViewServletConfiguration.class,
    DruidWebStatFilterConfiguration.class,
    DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {

    private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);

    @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public DataSource dataSource() {
        LOGGER.info("Init DruidDataSource");
        return new DruidDataSourceWrapper();
    }
}

到這裏就很明顯了,我們看到這個類用@Configuration註釋了,並且有@Bean註釋了datasource,所以相當在druid裏面裏經幫我們自動注入了datasource(當然這裏有一些springboot的註解,比如@COnditionOnclass等條件注入,大家可以自己網上查找資料或研究),另外大家可以自行研究druid如何將yml或者properties文件中的配置信息注入到datasource中,我就不跟大家一起了。分析到這裏,相信大家對springboot加載流程有一個整理的理解,同時也可以自定義starter啓動器,如果還是有一些問題,可以自己再跟一遍源碼,並且學習他人的自定義starter。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章