Mybatis-spring-boot-starter自動配置的原理分析


相信大家在使用SpringBoot的過程中,經常會使用到mybatis,通過使用mybatis-spring-boot-starter依賴進行自動配置,省去了自己依賴配置和Bean配置的很多麻煩。

有這么方便的starter,使大家不禁好奇,它究竟是怎么讓我們能夠不要任何配置就可以使用mybatis的,背后的原理究竟是什么?

本文將以mybatis-spring-boot-starter作為例子,探究它背后的秘密。

首先我們建立一個SpringBoot工程,並且添加mybatis-spring-boot-starter依賴(在此我們假設讀者已經熟悉SpringBoot基本知識和操作):

 

如果我們此時啟動程序,Mybatis會隨程序自動啟動。

 

starter減少了我們依賴配置和代碼中對象關系映射配置,我們分成兩部分分析:

第一步我們分析starter的依賴,從pom文件檢查工程的依賴:

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

 

可以看到,starter將需要的依賴全部引入,免去了我們配置的麻煩。

綜上所述,spring的starter免去了我們配置的麻煩,是個很好用的系統,全文完,謝謝觀看。

哈哈哈,如果文章寫到這里就結束,大家肯定要打我了(讀者內心:你寫的這什么東西)

光靠引入依賴包並不足以說明為什么這些組件就能自動被裝載,並且已經配置好了,所以我們分析下這些被引用的依賴,找到其中起作用的包。

首先mybatis,mybatis-spring都可以被忽略掉,這些就是正常的mybatis組件。

那可疑的就是這個mybatis-spring-boot-autoconfigure了。

直接打開這個jar包看里面的內容:

 

可疑的就是這個MybatisAutoConfiguration和spring.factories文件,其中spring.factories的內容:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

首先百度一下spring.factories,這個是Spring的SPI機制,這個文件的內容也已經說的很明顯了,自動配置,連注釋都給帶上了,哈哈。

代碼通過SPI機制加載了兩個類,其中最重要的是:

org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

我們打開這個類,查看其中代碼:

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

//......

可以看到,這是一個Spring配置類(因為有Configuration注解)

這個類需要在classpath中有SqlSessionFactory和SqlSessionFactoryBean定義時才會起效,並且要有一個DataSource的candidate注冊到spring容器中

當這些條件都滿足時,這個Configuration會注冊一個SqlSessionFactorySqlSessionTemplate :

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

......

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {

......

而MyBatis整個數據庫訪問,都是圍繞這兩個對象展開的。

至此已經解開Mybatis自動配置的第一個問題了,還有第二個:我們定義的那些Dao是如何被掃描的?

請繼續在這個類中往下看,類中間包裹了一個static subclass:

  /**
   * This will just scan the same base package as Spring Boot does. If you want more power, you can explicitly use
   * {@link org.mybatis.spring.annotation.MapperScan} but this will get typed mappers working correctly, out-of-the-box,
   * similar to using Spring Data JPA repositories.
   */
  public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {

    private BeanFactory beanFactory;

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

    //......
      registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
    }

這個類實現了ImportBeanDefinitionRegistrar,也就是會向Spring容器中注入一個bean定義,而這個bean定義就是MapperScannerConfigurer。

我們繼續看MapperScannerConfigurer:

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
//......

標紅的部分已經很能說明問題了:這個類是BeanDefinitionRegistry的后處理器,可能會修改Spring容器中的BeanDefinition,果然,在下面我們就能找到對應的方法:

  /**
   * {@inheritDoc}
   * 
   * @since 1.0.2
   */
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders();
    }

    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

其中最重要的部分就是這個標紅的scan,我們跟進去看下(其實不跟進去大家應該也看出所以然來了,這里有個basePackage,是不是很熟悉?):

/**
     * Perform a scan within the specified base packages,
     * returning the registered bean definitions.
     * <p>This method does <i>not</i> register an annotation config processor
     * but rather leaves this up to the caller.
     * @param basePackages the packages to check for annotated classes
     * @return set of beans registered if any for tooling registration purposes (never {@code null})
     */
    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;
    }

這里就是從basePackages中掃描到所有合適的類(准確的說應該是接口),並且將這些類(接口)生成對應的BeanDefinition,這是為了讓Spring在實例化你定義的Dao時,能夠成功,如果沒有這一步,啟動肯定會報“Cannot find candidate for autowired property xxx”這類錯誤,因為你在工程里面聲明的本來就是個接口嘛。

並且請注意到最后面標紅的applyScopedProxyMode,這會為你的Dao生成代理,這也是為何你調試代碼時,調用到Dao時你把鼠標放上去,顯示的Dao類型是一個MapperProxy<T>。

 

至此,Mybatis自動加載的原理基本上可以總結如下:

1. mybatis-spring-boot-starter將mybatis需要的依賴全部引入

2. starter同時通過SPI機制引入了一個配置Class:MybatisAutoConfiguration,它負責注冊SqlSessionFactory和SqlSessionTemplate到Spring容器中,我們使用Mybatis時絕大部分功能靠這兩個Bean實現

3. 引入了AutoConfiguredMapperScannerRegistrar這個bean到Spring容器,它負責將MapperScanner引入Spring容器,然后MapperScanner會將工程中所有的Dao掃描出來轉換為BeanDefinition並且注冊到Spring容器中

4. 當開發的工程中使用了某個Dao時,Spring能夠從容器中找到這個Dao對應的BeanDefinition,將其實例化並且注入,這樣開發者就可以使用了,這也是為何我們只定義了Dao的接口,但是工程運行時能夠有實例Bean的原因


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM