天天看點

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

看懂這篇文章可能需要有一定的spring源碼基礎,在分析原理之前,先簡單講一下應用。。。

使用spring boot結合mybatis開發時,會在pom.xml中引入下面依賴:

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.1</version>
</dependency>      

而這個依賴實際上引入了以下依賴:

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
    </dependency>
</dependencies>      

可以看到實際上引入了mybatis-spring ,這個jar包是為了讓mybatis與spring兩個架構更好的內建、更友善開發人員而開發的。

使用mybatis時,常用的标記Mapper所在位置的兩種方式為:

1.在每個Mapper(示例代碼的Dao層)檔案上加上@Mapper注解,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

2.在spring boot啟動類上加上@MapperScan注解,并指定包名,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

鄙人的代碼結構如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

下面将按照這兩種配置方式來分析mybatis是如何利用spring的擴充點的,首先分析 @Mapper方式,故事還得從spring初始化講起。。。

1.在spring初始化時,會調用org.springframework.context.support.AbstractApplicationContext#refresh,方法中有這樣一行代碼:

// Invoke factory processors registered as beans in the context.
// 調用 BeanFactory 後置處理器
invokeBeanFactoryPostProcessors(beanFactory);      

2.這行代碼會一直調用到 org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry,方法中有這樣一行代碼:

processConfigBeanDefinitions(registry);      

3.這行代碼會一直調用到 org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsFromRegistrars,代碼如下:

private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
        // 這行代碼的意思就是循環調用實作了ImportBeanDefinitionRegistrar的類的registerBeanDefinitions方法
    registrars.forEach((registrar, metadata) ->
        registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
}      

可以看到registrars中有一個元素,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

4.MybatisAutoConfiguration的靜态内部類AutoConfiguredMapperScannerRegistrar實作了ImportBeanDefinitionRegistrar,并重寫了registerBeanDefinitions()方法,主要代碼如下:

public static class AutoConfiguredMapperScannerRegistrar
      implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

      // 這句日志說明了這個方法的作用
      logger.debug("Searching for mappers annotated with @Mapper");

      ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

      try {
        if (this.resourceLoader != null) {
          scanner.setResourceLoader(this.resourceLoader);
        }

        // 拿到需要掃描的包名,預設是頂層包,鄙人的為:com.example.demo 
        List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
        if (logger.isDebugEnabled()) {
          for (String pkg : packages) {
            logger.debug("Using auto-configuration base package '{}'", pkg);
          }
        }

        // 設定AnnotationClass: Mapper.class
        scanner.setAnnotationClass(Mapper.class);
        // 注冊過濾器,會把包含了 Mapper.class 的過濾器注冊進去
        scanner.registerFilters();
        // 掃描包
        scanner.doScan(StringUtils.toStringArray(packages));
      } catch (IllegalStateException ex) {
        logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex);
      }
    }
}      

5.調用org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法來掃描類,代碼如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
        // 掃描類路徑以查找候選元件
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
        candidate.setScope(scopeMetadata.getScopeName());
        String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
        if (candidate instanceof AbstractBeanDefinition) {
          postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
        }
        if (candidate instanceof AnnotatedBeanDefinition) {
          AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
        }
        if (checkCandidate(beanName, candidate)) {
          BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
          definitionHolder =
              AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
          beanDefinitions.add(definitionHolder);
          registerBeanDefinition(definitionHolder, this.registry);
        }
      }
    }
    return beanDefinitions;
  }      

6.進入 findCandidateComponents(basePackage) 方法,這個方法是其父類ClassPathScanningCandidateComponentProvider.java 中的方法,方法中又調用了 scanCandidateComponents() 方法,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

先看上面那個 isCandidateComponent(metadataReader) ,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

隻有這個方法傳回true時才會繼續執行第二個 isCandidateComponent(sbd) ,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

滿足這兩個條件才會将符合條件的類添加到candidates中,在示例代碼中,最終有兩個符合條件的類:MenuDao和StudentDao,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

7.完成Mapper(示例代碼的Dao層類)的掃描後,回到org.mybatis.spring.mapper.ClassPathMapperScanner#doScan,繼續完成BeanDefinition的處理,調用 processBeanDefinitions(beanDefinitions) ,這個方法中有一個很重要的操作:将自動注入模式設定為 byType,這個操作對使用@Autowired注解注入Mapper時有重大意義,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

完成這個操作後,按照spring初始化流程了解即可。

接下來分析第二種方式: @MapperScan(basePackages = “com.example.demo.dao”)

1.檢視 @MapperScan 注解:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

這個注解使用 @Import 引入了 MapperScannerRegistrar.class ,這個@Import 的作用可以簡單了解為讓 @MapperScan 擁有 MapperScannerRegistrar 的功能,是以重點看一下 MapperScannerRegistrar.class ,如下:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

MapperScannerRegistrar 也實作了 ImportBeanDefinitionRegistrar ,并重寫了registerBeanDefinitions()

2.看到 registerBeanDefinitions() 方法最後的代碼:

淺析Mybatis利用Spring擴充點之ImportBeanDefinitionRegistrar

這時候 basePackages = “com.example.demo.dao” ,然後也會調用org.mybatis.spring.mapper.ClassPathMapperScanner#doScan 方法來掃描,後續的流程基本上與第一種方式相同了。

通過兩種方式對比發現:用 @MapperScan 來指定 Mapper 所在包名時,在掃描前得到的 basePackages 範圍更小,是以掃描時效率會更高。

在使用mybatis時,如果結合了注解和xml兩種方式來編寫sql,需要在spring boot配置檔案中指定xml的路徑,如下:

mybatis:
  mapper-locations: classpath:mapping/*.xml      

示例就是同時使用了兩種方式,這種情況下,先處理Mapper.java,後處理Mapper.xml ,那麼xml是什麼時候處理的呢?

在調用org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory建立org.apache.ibatis.session.SqlSessionFactory對象時,會調用到org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory,在這個方法中會判斷 mapperLocations 是否為空,對應上面那個配置,如果不為空則會循環解析xml,如下: