SpringBoot2.x基礎篇:帶你瞭解掃描Package自動註冊Bean

知識改變命運,擼碼使我快樂,2020繼續遊走在開源界<br/>
點贊再看,養成習慣<br/>
給我來個Star吧, 點擊瞭解下基於SpringBoot的組件化接口服務落地解決方案

咱們一直在使用SpringBoot來開發應用程序,可是爲何在項目啓動時就會自動註冊使用註解@Component@Service@RestController...標註的Bean呢?html

推薦閱讀

默認掃描目錄

SpringBoot把入口類所在的Package做爲了默認的掃描目錄,這也是一個約束,若是咱們把須要被註冊到IOC的類建立在掃描目錄下就能夠實現自動註冊,不然則不會被註冊。java

若是你入口類叫作ExampleApplication,它位於org.minbox.chapter目錄下,當咱們啓動應用程序時就會自動掃描org.minbox.chapter同級目錄子級目錄下所有註解的類,以下所示:git

. src/main/java
├── org.minbox.chapter
│   ├── ExampleApplication.java
│   ├── HelloController.java
│   ├── HelloExample.java
│   └── index
│   │   └── IndexController.java
├── com.hengboy
│   ├── TestController.java
└──

HelloController.javaHelloExample.java與入口類ExampleApplication.java在同一級目錄下,因此在項目啓動時能夠被掃描到spring

IndexController.java則是位於入口類的下級目錄org.minbox.chapter.index內,由於支持下級目錄掃描,因此它也能夠被掃描到segmentfault

TestController.java位於com.hengboy目錄下,默認沒法掃描到api

自定義掃描目錄

在上面目錄結構中位於com.hengboy目錄下的TestController.java類,默認狀況下是沒法被掃描並註冊到IOC容器內的,若是想要掃描該目錄下的類,下面有兩種方法。架構

方法一:使用@ComponentScan註解app

@ComponentScan({"org.minbox.chapter", "com.hengboy"})

方法二:使用scanBasePackages屬性框架

@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})
注意事項:配置自定義掃描目錄後, 會覆蓋掉默認的掃描目錄,若是你還須要掃描默認目錄,那麼你要進行配置掃描目錄,在上面自定義配置中,若是僅配置掃描 com.hengboy目錄,則 org.minbox.chapter目錄就不會被掃描。

追蹤源碼

下面咱們來看下SpringBoot源碼是怎麼實現自動化掃描目錄下的Bean,並將Bean註冊到容器內的過程。spring-boot

因爲註冊的流程比較複雜,挑選出具備表明性的流程步驟來進行講解。

獲取BasePackages

org.springframework.context.annotation.ComponentScanAnnotationParser#parse方法內有着獲取basePackages的業務邏輯,源碼以下所示:

Set<String> basePackages = new LinkedHashSet<>();
// 獲取@ComponentScan註解配置的basePackages屬性值
String[] basePackagesArray = componentScan.getStringArray("basePackages");
// 將basePackages屬性值加入Set集合內
for (String pkg : basePackagesArray) {
  String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
                                                         ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
  Collections.addAll(basePackages, tokenized);
}
// 獲取@ComponentScan註解的basePackageClasses屬性值
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
  // 獲取basePackageClasses所在的package並加入Set集合內
  basePackages.add(ClassUtils.getPackageName(clazz));
}
// 若是並無配置@ComponentScan的basePackages、basePackageClasses屬性值
if (basePackages.isEmpty()) {
  // 使用Application入口類的package做爲basePackage
  basePackages.add(ClassUtils.getPackageName(declaringClass));
}

獲取basePackages分爲了那麼三個步驟,分別是:

  1. 獲取@ComponentScan註解basePackages屬性值
  2. 獲取@ComponentScan註解basePackageClasses屬性值
  3. Application入口類所在的package做爲默認的basePackages
注意事項:根據源碼也就證明了,爲何咱們配置了 basePackagesbasePackageClasses後會把默認值覆蓋掉,這裏其實也不算是覆蓋,是根本不會去獲取 Application入口類的 package

掃描Packages下的Bean

獲取到所有的Packages後,經過org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法來掃描每個Package下使用註冊註解(@Component@Service@RestController...)標註的類,源碼以下所示:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
  // 當basePackages爲空時拋出IllegalArgumentException異常
  Assert.notEmpty(basePackages, "At least one base package must be specified");
  Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
  // 遍歷每個basePackage,掃描package下的所有Bean
  for (String basePackage : basePackages) {
    // 獲取掃描到的所有Bean
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    // 遍歷每個Bean進行處理註冊相關事宜
    for (BeanDefinition candidate : candidates) {
      // 獲取做用域的元數據
      ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
      candidate.setScope(scopeMetadata.getScopeName());
      // 獲取Bean的Name
      String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
      if (candidate instanceof AbstractBeanDefinition) {
        postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
      }
      // 若是是註解方式註冊的Bean
      if (candidate instanceof AnnotatedBeanDefinition) {
        // 處理Bean上的註解屬性,相應的設置到BeanDefinition(AnnotatedBeanDefinition)類內字段
        AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
      }
      // 檢查是否知足註冊的條件
      if (checkCandidate(beanName, candidate)) {
        // 聲明Bean具有的基本屬性
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        // 應用做用域代理模式
        definitionHolder =
          AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        // 寫入返回的集合
        beanDefinitions.add(definitionHolder);
        // 註冊Bean
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

在上面源碼中會掃描每個basePackage下經過註解定義的Bean,獲取Bean註冊定義對象後並設置一些基本屬性。

註冊Bean

掃描到basePackage下的Bean後會直接經過org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition方法進行註冊,源碼以下所示:

public static void registerBeanDefinition(
  BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
  throws BeanDefinitionStoreException {

  // 註冊Bean的惟一名稱
  String beanName = definitionHolder.getBeanName();
  // 經過BeanDefinitionRegistry註冊器進行註冊Bean
  registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

  // 若是存在別名,進行註冊Bean的別名
  String[] aliases = definitionHolder.getAliases();
  if (aliases != null) {
    for (String alias : aliases) {
      registry.registerAlias(beanName, alias);
    }
  }
}

經過org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition註冊器內的方法能夠直接將Bean註冊到IOC容器內,而BeanName則是它生命週期內的惟一名稱。

總結

經過本文的講解我想你應該已經瞭解了SpringBoot應用程序啓動時爲何會自動掃描package並將Bean註冊到IOC容器內,雖然項目啓動時間很短暫,不過這是一個很是複雜的過程,在學習過程當中你們能夠使用Debug模式來查看每個步驟的邏輯處理。

做者我的 博客
使用開源框架 ApiBoot 助你成爲Api接口服務架構師
相關文章
相關標籤/搜索