天天看點

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

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

我們一直在使用

SpringBoot

來開發應用程式,但是為什麼在項目啟動時就會自動注冊使用注解

@Component

@Service

@RestController

...标注的

Bean

呢?

預設掃描目錄

SpringBoot

把入口類所在的

Package

作為了預設的掃描目錄,這也是一個限制,如果我們把需要被注冊到

IOC

的類建立在掃描目錄下就可以實作自動注冊,否則則不會被注冊。

如果你入口類叫做

ExampleApplication

,它位于

org.minbox.chapter

目錄下,當我們啟動應用程式時就會自動掃描

org.minbox.chapter

同級目錄、子級目錄下全部注解的類,如下所示:

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

複制

HelloController.java

HelloExample.java

與入口類

ExampleApplication.java

在同一級目錄下,是以在項目啟動時可以被掃描到。

IndexController.java

則是位于入口類的下級目錄

org.minbox.chapter.index

内,因為支援下級目錄掃描,是以它也可以被掃描到。

TestController.java

位于

com.hengboy

目錄下,預設無法掃描到。

自定義掃描目錄

在上面目錄結構中位于

com.hengboy

目錄下的

TestController.java

類,預設情況下是無法被掃描并注冊到

IOC

容器内的,如果想要掃描該目錄下的類,下面有兩種方法。

方法一:使用@ComponentScan注解

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

複制

方法二:使用scanBasePackages屬性

@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})           

複制

注意事項:配置自定義掃描目錄後,會覆寫掉預設的掃描目錄,如果你還需要掃描預設目錄,那麼你要進行配置掃描目錄,在上面自定義配置中,如果僅配置掃描

com.hengboy

目錄,則

org.minbox.chapter

目錄就不會被掃描。

追蹤源碼

下面我們來看下

SpringBoot

源碼是怎麼實作自動化掃描目錄下的

Bean

,并将

Bean

注冊到容器内的過程。

由于注冊的流程比較複雜,挑選出具有代表性的流程步驟來進行講解。

擷取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

注意事項:根據源碼也就證明了,為什麼我們配置了

basePackages

basePackageClasses

後會把預設值覆寫掉,這裡其實也不算是覆寫,是根本不會去擷取

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接口服務架構師