mybatis-spring註解MapperScan的原理

2023-03-21 15:06:54

很多開發者應該都知道,我們只使用@MapperScan這個註解就可以把我們寫的Mybatis的Mapper介面載入到Spring的容器中,不需要對每個Mapper介面加@Mapper這個註解了,加快了我們開發的效率。如下:

就可以把我們寫在io.renren.mapper這個包下的Mapper介面載入到我們的Spring容器中。當然mybatis-spring能使用這樣的註解還是因為的大神開發者給我們提供了大量的可延伸的介面。下面就聊聊它的原理​就是@MapperScan這個註解如下:

/*
* @since 1.2.0
 * @see MapperScannerRegistrar  
 * @see MapperFactoryBean // 這個類貼出來它很重要
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {

好傢伙又是@Import這個類幫助我們匯入了MapperScannerRegistrar【PS這個類前面文章也多次用到,它的3個作用我前面的文章也都說過了,有興趣可以翻看我的前面寫的文章】,這個類的程式碼如下:

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,
          generateBaseBeanName(importingClassMetadata, 0));
    }
  }
  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    // 註冊了一個 MapperScannerConfigurer的BeanDefinition
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
  }

它主要是幫我們注入了一個MapperScannerConfigurer型別的BeanDefinition 。那我們就先看這個類的相關程式碼如下:

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor{
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    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);

    scanner.registerFilters();
    // 開始掃描指定類下的Mapper介面
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

如上面的程式碼 MapperScannerConfigurer這個類實現了BeanDefinitionRegistryPostProcessor【以前的文章也有提到】

是Spring提供給開發者供開發者實現這個介面然後可以介入Spring的啟動週期,具體Spring是什麼時候執行這段程式碼的呢?教大家一個好的方法如下:

我們斷點打到上面的位置,然後Debug啟動專案,然就可以看到這個方法的呼叫路徑如下,經過5步就呼叫到這個方法了。

第一個方法AbstractApplicationContext.refresh方法應該是Spring中最重要的方法了,Spring的很多很多功能都是在這個方法裡面完成的,有興趣的可以讀一下,下面貼出來直接回撥的這個方法如下:

/**
   * Invoke the given BeanDefinitionRegistryPostProcessor beans.
   * 這個方法就會回撥MapperScannerConfigurer的
   * postProcessBeanDefinitionRegistry方法
   */
  private static void invokeBeanDefinitionRegistryPostProcessors(
      Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
    for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
      postProcessor.postProcessBeanDefinitionRegistry(registry);
    }
  }

幫大家找到Spring回撥的方法了,就還回到MapperScannerConfigurer的postProcessBeanDefinitionRegistry方法。我們主要看scan【掃描】這個方法,進入這個方法如下:

Spring大神寫程式碼有個特點方法前面帶do就是真正工作的方法【PS我們寫複雜業務的時候也可以模仿do....方法】因為do有做,執行的意思。程式碼如下:

/**
    在我們指定的包內執行
   * Perform a scan within the specified base packages,
   // 返回掃描到的 bean definitions. 
   * returning the registered bean definitions.
   */
  protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    // 掃描到的beanDefinitions 放到這裡
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
    // 獲取候選的元件【我們寫的Mapper介面】,主要的邏輯都在這裡
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      beanDefinitions.add(definitionHolder);
    }
    return beanDefinitions;
  }

findCandidateComponents方法如下:

 
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
    // 根據我們寫的包名獲取Mapper介面在專案中的實際路徑
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
          resolveBasePackage(basePackage) + '/' + this.resourcePattern;
          // 獲取包下的所有介面的資源
      Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
      for (Resource resource : resources) {
        if (resource.isReadable()) {
          try {
            MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
            if (isCandidateComponent(metadataReader)) {
              ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
              if (isCandidateComponent(sbd)) {
                // 把我們寫的Dao或者Mapper介面放入candidates中
                candidates.add(sbd);
              }
            }
    }
   // 把封裝的Mapper的 BeanDefinition返回
    return candidates;
  }

把封裝好的BeanDefinition封裝好返回後,後面就會進入Spring Bean的生命週期進行Bean的初始化,然後就提供給開發者直接使用了。到這裡有的小夥伴該問了,我們寫的是介面Spring怎麼能給介面初始化呢?上面這些都是瞎聊的。如果能這樣問說明你的Java基礎很不錯哦,值得鼓勵。那小夥伴是否記得我文章開始說 MapperFactoryBean這個類呢?我還特別強調這個類很重要,這個類是FactoryBean。Mybatis是把我們寫的介面生成了的代理物件MapperProxy 。由於我使用的是MybatisPlus,它又封裝一下MapperProxy變成了MybaitsMapperProxy,這個類直接copy了Mybatis的程式碼。我們通過MapperFactoryBean的getObject()方法就能獲取MapperProxy了,它不是介面

FactoryBean和BeanFactory的區別:

FactoryBean,是Spring提供的一個擴充套件點,用於複雜Bean的建立。mybatsi在跟Spring做整合時候就用到了這個擴充套件點。並且FactoryBean所建立的Bean跟普通的Bean不一樣。它有getObject()方法才可以獲取具體的Bean

BeanFactory是Spring IOC容器的頂級介面,其實現類有XMLBeanFactory ,DefaultListBeanFactory。BeanFactory為Srping管理Bean提供了一套通用的介面規範。

如果不信可以看如下,圖中解釋的很明白了:

上面就是MapperScan的整個原理,如果跟著上面的程式碼走一遍流程相信你對它的原理就更懂了,你寫程式碼的時候對寫的Mapper介面應該就知道不加@Mapper這個註解Spring也能幫你完成你寫的Mapper介面的初始化。

再分享一個Spring生成UUID的很高效的工具類:AlternativeJdkIdGenerator。

大意是它使用了SecureRandom作為種子,來替換呼叫UUID#randomUUID()。它提供了一個更好、更高效能的表現  發現僅僅迴圈生成1000萬次,Spring提供的演演算法效能遠遠高於JDK的。因此建議大家以後使用AlternativeJdkIdGenerator去生成UUID,效能會更好一點          

 缺點是:還需要new物件才能使用,不能通過類名直接呼叫靜態方法,當然我們可以二次封裝。另外,一般輸出串我們都會進一步這麼處理:.toString().replace("-", "")

如下對比一下就知道它的效能有多好了。

  /**
         * 直接呼叫java的那個UUID工具 
         */
        JdkIdGenerator jdkIdGenerator = new JdkIdGenerator();
        AlternativeJdkIdGenerator alternativeJdkIdGenerator = new AlternativeJdkIdGenerator();
        Instant start;
        Instant end;
        int count = 10000000;
        //jdkIdGenerator
        start = Instant.now();
        for (int i = 0; i < count; i++) {
            jdkIdGenerator.generateId();
        }
        end = Instant.now();
        System.out.println("jdkIdGenerator迴圈" + count + "次耗時:" + Duration.between(start, end).toMillis() + "ms");
        //alternativeJdkIdGenerator
        start = Instant.now();
        for (int i = 0; i < count; i++) {
            alternativeJdkIdGenerator.generateId();
        }
        end = Instant.now();
        System.out.println("alternativeJdkIdGenerator迴圈" + count + "次耗時:" + Duration.between(start, end).toMillis() + "ms");

如下結果如下Spring的效能太高了。