Spring整合MyBatis原理-@MapperScan

https://mybatis.org/spring/zh/mappers.html

mybatis-spring为我们提供了注册Mapper接口的2种方式

  • 手动配置MapperFactoryBean
  • @MapperScan自动扫描发现

手动的方式

@Configuration
public class MyBatisConfig {
  @Bean
  public MapperFactoryBean<UserMapper> userMapper() throws Exception {
    MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
    factoryBean.setSqlSessionFactory(sqlSessionFactory());
    return factoryBean;
  }
}

我们有几个Mapper接口就需要配置几个这种Bean,相当麻烦,@MapperScan帮我们省去了这一工作,但本质上也是自动帮我们为每个Mapper接口注册这样的一个Bean。

MapperFactoryBean本篇文章不会说其原理。大家只要知道,这种方式可以将我们的Mapper接口注册成为Bean即可。 下面我们就来看下@MapperScan是如何自动帮我们为每个Mapper接口生成一个MapperFactoryBean并注册到Spring容器中的。

@MapperScan

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan 
{

  /**
   * basePackages属性的别名
   */

  @AliasFor("basePackages")
  String[] value() default {};

  /**
   * 指定扫描的包路径
   */

  @AliasFor("value")
  String[] basePackages() default {};

  /**
   * 通过类全限定名指定扫描包路径
   */

  Class<?>[] basePackageClasses() default {};

  /**
   * @link BeanNameGenerator
   */

  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  /**
   * 指定注解,被标注指定注解的接口才会被看做成Mapper接口被扫描
   */

  Class<? extends Annotation> annotationClass() default Annotation.class;

  /**
   * 指定接口类型,被指定的接口及其子接口才会被看做成 Mapper接口被扫描
   */

  Class<?> markerInterface() default Class.class;

  /**
   * SqlSessionTemplate的beanName指定
   */

  String sqlSessionTemplateRef() default "";

  /**
   * sqlSessionFactory的beanName指定
   */

  String sqlSessionFactoryRef() default "";

  /**
   * 指定MapperFactoryBean类型,允许用户自定义但需要继承MapperFactoryBean
   */

  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

  /**
   * 是否延迟初始化
   */

  String lazyInitialization() default "";

  /**
   * 指定默认的Scope
   */

  String defaultScope() default AbstractBeanDefinition.SCOPE_DEFAULT;

}

下面我们查看一下@Import引入的类MapperScannerRegistrar

MapperScannerRegistrar

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrarResourceLoaderAware {
  @Override
  @Deprecated
  public void setResourceLoader(ResourceLoader resourceLoader) {
    // NOP
  }


  @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);
    builder.addPropertyValue("processPropertyPlaceHolders"true);

    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
      builder.addPropertyValue("annotationClass", annotationClass);
    }

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      builder.addPropertyValue("markerInterface", markerInterface);
    }

    Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
    if (!BeanNameGenerator.class.equals(generatorClass)) {
      builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
    }

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
    }

    String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
    if (StringUtils.hasText(sqlSessionTemplateRef)) {
      builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
    }

    String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
    if (StringUtils.hasText(sqlSessionFactoryRef)) {
      builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
    }

    List<String> basePackages = new ArrayList<>();

    basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
        .collect(Collectors.toList()));

    basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
        .collect(Collectors.toList()));

    if (basePackages.isEmpty()) {
      basePackages.add(getDefaultBasePackage(annoMeta));
    }

    String lazyInitialization = annoAttrs.getString("lazyInitialization");
    if (StringUtils.hasText(lazyInitialization)) {
      builder.addPropertyValue("lazyInitialization", lazyInitialization);
    }

    String defaultScope = annoAttrs.getString("defaultScope");
    if (!AbstractBeanDefinition.SCOPE_DEFAULT.equals(defaultScope)) {
      builder.addPropertyValue("defaultScope", defaultScope);
    }

    builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

    builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }

  private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
    return importingClassMetadata.getClassName() + "#" + MapperScannerRegistrar.class.getSimpleName() + "#" + index;
  }

  private static String getDefaultBasePackage(AnnotationMetadata importingClassMetadata) {
    return ClassUtils.getPackageName(importingClassMetadata.getClassName());
  }


  static class RepeatingRegistrar extends MapperScannerRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      AnnotationAttributes mapperScansAttrs = AnnotationAttributes
          .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScans.class.getName()));
      if (mapperScansAttrs != null) {
        AnnotationAttributes[] annotations = mapperScansAttrs.getAnnotationArray("value");
        for (int i = 0; i < annotations.length; i++) {
          registerBeanDefinitions(importingClassMetadata, annotations[i], registry,
              generateBaseBeanName(importingClassMetadata, i));
        }
      }
    }
  }

}

MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar,自然就看其registerBeanDefinitions方法,这个方法注册了一个BeanMapperScannerConfigurer,我们看看其流程

  1. 获取MapperScannerConfigurerBeanDefinitionBuilder
  2. 设置属性processPropertyPlaceHolderstrue
  3. 设置属性annotationClass@MapperScan#annotationClass
  4. 设置属性markerInterface@MapperScan#markerInterface
  5. 设置属性nameGenerator@MapperScan#nameGenerator
  6. 设置属性mapperFactoryBeanClass@MapperScan#factoryBean
  7. 设置属性sqlSessionTemplateBeanName@MapperScan#sqlSessionTemplateRef
  8. 设置属性sqlSessionFactoryBeanName@MapperScan#sqlSessionFactoryRef
  9. 设置属性lazyInitialization@MapperScan#lazyInitialization
  10. 设置属性defaultScope@MapperScan#defaultScope
  11. 设置属性basePackage
  12. 设置BeanDefinition角色
  13. 注册MapperScannerConfigurerBeanDefinition

最终注册了MapperScannerConfigurer

扫描包的设置

@MapperScan的作用就是注册MapperScannerConfigurer,其中会指定扫描包,我们来看一下其扫描包怎么选取的

  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName)
 
{

 //.....................

    basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText)
        .collect(Collectors.toList()));

    basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
        .collect(Collectors.toList()));

    if (basePackages.isEmpty()) {
      basePackages.add(getDefaultBasePackage(annoMeta));
    }

 //..............

    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }
  1. 读取@MapperScan#basePackages中的数据
  2. 读取@MapperScan#basePackageClasses中的数据,并通过ClassUtils::getPackageName解析成包名
  3. 若最终basePackages变量为空(@MapperScan既没有指定basePackages也没有指定basePackageClasses),则通过 getDefaultBasePackage设置扫描包

ClassUtils::getPackageName会将指定的类所在的包作为扫描包

 public static String getPackageName(Class<?> clazz) {
  Assert.notNull(clazz, "Class must not be null");
  return getPackageName(clazz.getName());
 }
 public static String getPackageName(String fqClassName) {
  Assert.notNull(fqClassName, "Class name must not be null");
  int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR);
  return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
 }

getDefaultBasePackage(annoMeta)若没指定扫描包,@MapperScan标注的类所在的包作为扫描包

private static String getDefaultBasePackage(AnnotationMetadata importingClassMetadata) {
return ClassUtils.getPackageName(importingClassMetadata.getClassName());
}


public static String getPackageName(String fqClassName) {
Assert.notNull(fqClassName, "Class name must not be null");
int lastDotIndex = fqClassName.lastIndexOf(PACKAGE_SEPARATOR);
return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
}


MapperScannerConfigurer

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessorInitializingBeanApplicationContextAwareBeanNameAware 
{

  /**
   * {@inheritDoc}
   */

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
      //......
  }

}

可以看到MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor,因此会在Spring应用上下文刷新时invokeBeanFactoryPostProcessors(beanFactory);方法中被调用。

MapperScannerConfigurer#postProcessBeanDefinitionRegistry

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessorInitializingBeanApplicationContextAwareBeanNameAware 
{

  private String basePackage;

  private boolean addToConfig = true;

  private String lazyInitialization;

  private SqlSessionFactory sqlSessionFactory;

  private SqlSessionTemplate sqlSessionTemplate;

  private String sqlSessionFactoryBeanName;

  private String sqlSessionTemplateBeanName;

  private Class<? extends Annotation> annotationClass;

  private Class<?> markerInterface;

  private Class<? extends MapperFactoryBean> mapperFactoryBeanClass;

  private ApplicationContext applicationContext;

  private String beanName;

  private boolean processPropertyPlaceHolders;

  private BeanNameGenerator nameGenerator;

  private String defaultScope;


  @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));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    scanner.registerFilters();
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

}

代码相当易懂,就是创建ClassPathMapperScanner对象并调用scan方法进行扫描包

ClassPathMapperScanner

package org.mybatis.spring.mapper;
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
}
package org.springframework.context.annotation;

public class ClassPathBeanDefinitionScanner 
   extends ClassPathScanningCandidateComponentProvider 
{
    
}

可以看到ClassPathMapperScannermybatis-spring框架继承Spring的ClassPathBeanDefinitionScanner所实现的类

ClassPathBeanDefinitionScanner#scan

ClassPathMapperScanner并没有重写ClassPathBeanDefinitionScannerscan方法

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

 public int scan(String... basePackages) {
  int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

  doScan(basePackages);

  // Register annotation config processors, if necessary.
  if (this.includeAnnotationConfig) {
   AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
  }

  return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
 }
}

这段代码的逻辑是:

  1. 前后获取BeanDefinition的数量返回差值
  2. AnnotationConfigUtils.registerAnnotationConfigProcessors:用于注册一些注解相关的BeanPostProcessor
  3. 最后看doScan

ClassPathMapperScanner#doScan

  @Override
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
      LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
          + "' package. Please check your configuration.");
    } else {
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }
  1. 调用super.doScan(basePackages):就是调用ClassPathBeanDefinitionScanner#doScan

默认情况下ClassPathBeanDefinitionScanner#doScan不会扫描接口类,通过ClassPathScanningCandidateComponentProvider#isCandidateComponent()过滤掉扫描包内的所有接口,但是ClassPathMapperScanner重写了isCandidateComponent,只扫描所有接口类。因此beanDefinitions集合中是所有接口类型的BeanDefinition

  1. 执行ClassPathMapperScanner#processBeanDefinitions

ClassPathMapperScanner#processBeanDefinitions

这里再把手动注册MapperFactoryBean的方法放在这边,可以看到MapperFactoryBean需要一个mapperInterface

@Configuration
public class MyBatisConfig {
  @Bean
  public MapperFactoryBean<UserMapper> userMapper() throws Exception {
    MapperFactoryBean<UserMapper> factoryBean = new MapperFactoryBean<>(UserMapper.class);
    factoryBean.setSqlSessionFactory(sqlSessionFactory());
    return factoryBean;
  }
}
public class MapperFactoryBean<Textends SqlSessionDaoSupport implements FactoryBean<T{

  private Class<T> mapperInterface;

  public MapperFactoryBean(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

}

我们看一下ClassPathMapperScanner#processBeanDefinitions

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        AbstractBeanDefinition definition;
        BeanDefinitionRegistry registry = getRegistry();
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (AbstractBeanDefinition) holder.getBeanDefinition();
            
            String beanClassName = definition.getBeanClassName();


            try {
                // for spring-native
                definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
            } catch (ClassNotFoundException ignore) {
            }

            definition.setBeanClass(this.mapperFactoryBeanClass);

    
            definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

            boolean explicitFactoryUsed = false;
            if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
                definition.getPropertyValues().add("sqlSessionFactory",
                                                   new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
                explicitFactoryUsed = true;
            } else if (this.sqlSessionFactory != null) {
                definition.getPropertyValues().add("sqlSessionFactory"this.sqlSessionFactory);
                explicitFactoryUsed = true;
            }

            if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
                if (explicitFactoryUsed) {
                    LOGGER.warn(
                        () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
                }
                definition.getPropertyValues().add("sqlSessionTemplate",
                                                   new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
                explicitFactoryUsed = true;
            } else if (this.sqlSessionTemplate != null) {
                if (explicitFactoryUsed) {
                    LOGGER.warn(
                        () -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
                }
                definition.getPropertyValues().add("sqlSessionTemplate"this.sqlSessionTemplate);
                explicitFactoryUsed = true;
            }

            
            if (!explicitFactoryUsed) {
                definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            }
            definition.setLazyInit(lazyInitialization);
            //...........
        }
    }    
}

上面代码我也是截取了部分好理解的,遍历所有接口的BeanDefinition

  1. 获取beanClassName并设置在BeanDefinitionmapperInterface属性中
  2. 重新设置BeanDefinitonbeanClass属性为mapperFactoryBeanClass
  3. 设置sqlSessionFactory或者sqlSessionTemplateBeanDefinition
  • sqlSessionFactorysqlSessionTemplate必须被设置一个,否则会开启byType自动装配
  • sqlSessionFactorysqlSessionTemplate同时配置时,sqlSessionFactory会被sqlSessionTemplate取代

上面代码就是偷天换日,本来扫描到的BeanDefinition的class类型为XxxMapper接口.class。最后被换成MapperFactoryBean类型,并且XxxMapper接口.class被设置到mapperInterface属性中。

总结

好的,@MapperScan的原理就聊到这里,细节方面我们不去深究,我们可以总结一下其大致原理。

  1. mybatis-spring框架通过@MapperScan引入MapperScannerConfigurer
  2. MapperScannerConfigurer在容器刷新时,通过ClassPathMapperScanner扫描指定包下的所有接口,生成接口对应BeanDefinition
  3. 将所有扫描到接口对应BeanDefinitionbeanClass全部换成mapperFactoryBeanClass,并设置mapperInterface为相应接口类型。
  4. 最终,注册的是一个个的MapperFactoryBean类型。


原文始发于微信公众号(溪溪技术笔记):Spring整合MyBatis原理-@MapperScan

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/207129.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!