Detailed Explanation of Mapper Injection in MyBatis

  • 2021-11-14 05:37:13
  • OfStack

In the SpringBoot system, there are two common ways for MyBatis to inject Mapper:

1. @ MapperScan

The MapperScan class is in the mybatis-spring package.

By using @ MapperScan on the startup class, and then specifying the directory where the Mapper files are located through the basePackages attribute, all. java files in the specified directory will be loaded as Mapper by default.


@MapperScan(basePackages = "com.test.springboot.mapper")
@ServletComponentScan(basePackages = "com.test.springboot.filters")
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

As you can see, @ Import (MapperScannerRegistrar. class) is used on the MapperScan annotation, which means that MapperScannerRegistrar is injected into the Spring container as a configuration class.


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

The MapperScannerRegistrar class is an implementation of ImportBeanDefinitionRegistrar, which is actively triggered by Spring after the injection Spring container is created. The overloading method is to create and register an registry of MapperScannerConfigurer type. This registry mainly scans the specified files in the specified basePackages directory, and loads them into BeanDefinition and injects them into Spring container.


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

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

The following is the main implementation of MapperScannerConfigurer, which mainly relies on ClassPathMapperScanner to realize scanning. If basePackages is specified by MapperScan, it will only scan this specified directory, otherwise it may scan the whole classpath (similar to the complete scanning of SpringBoot).


@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));
  }

In the implementation of ClassPathMapperScanner, we can see that he will set beanClass of BeanDefinition of the scanned target class (such as xxxMapper. java annotated with @ Mapper) to MapperFactoryBean, and then Bean created according to BeanDefinition is the type of MapperFactoryBean. Because MapperFactoryBean is a factory class, when SpringBoot instantiates xxxMapper, it will judge that Bean corresponding to xxxMapper is a factory class, and then call its getObject method to create an instance of xxxMapper. java (of course, it must be a proxy class here).


private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;

  public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
    this.mapperFactoryBeanClass = mapperFactoryBeanClass == null ? MapperFactoryBean.class : mapperFactoryBeanClass;
  }

  /**
   * Calls the parent search that will search and register all the candidates. Then the registered objects are post
   * processed to set them as MapperFactoryBeans
   */
  @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;
  }

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();
              // ...      String beanClassName = definition.getBeanClassName();
      definition.setBeanClass(this.mapperFactoryBeanClass);

      definition.getPropertyValues().add("addToConfig", this.addToConfig);

      // Attribute for MockitoPostProcessor
      // https://github.com/mybatis/spring-boot-starter/issues/475
      definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

      // ...if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
        definition.setScope(defaultScope);
      }

      if (!definition.isSingleton()) {
        BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
        if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
          registry.removeBeanDefinition(proxyHolder.getBeanName());
        }
        registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
      }

    }
  }

 @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

The getObject method first obtains its SqlSessionTemplate instance, then obtains the MapperProxy instance corresponding to xxxMapper according to mapperInterface (this is the fully qualified name of xxxMapper. java), and then the method calls to xxxMapper class will go to MapperProxy step by step because of proxy- > SqlSessionTemplate - > sqlSessionProxy (1 proxy instance of SqlSession).

2. @ Mapper

The Mapper class is within the mybatis package.

It is definitely useless to simply add the annotation of @ Mapper to the class. Here we need the assistance of another official project mybatis-spring-boot-autoconfigure (this is an automatic configuration project, so we need the support of SpringBoot. In other words, the project needs to add the official spring-boot-configuration-processor dependency of Spring), so that only @ Mapper annotation can be added.

mybatis-spring-boot-starter dependencies can be used directly for ease of use of dependencies


<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>${mybatis-spring.version}</version>
</dependency>

We can find the files for spring. factories in the META-INF directory that mybatis-spring-boot-autoconfigure depends on, which SpringBoot actively scans for target files that require automatic configuration injection. You can see the protagonist MybatisAutoConfiguration class, which loads the configuration file of Mybatis, declares that it relies on some Bean, etc., and then you can find that it actively injects a class called AutoConfiguredMapperScannerRegistrar through @ Import.


# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
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 {    // ...
    @org.springframework.context.annotation.Configuration
    @Import(AutoConfiguredMapperScannerRegistrar.class)
    @ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
      @Override
      public void afterPropertiesSet() {
        logger.debug(
            "Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
      }
    }    // ...
}

The injected AutoConfiguredMapperScannerRegistrar is somewhat similar to the previous MapperScannerRegistrar, which is a registrar for scanning classes. It also registers MapperScannerConfigurer here, but the difference is that it explicitly specifies that the file scanned with Mapper annotation is scanned here, and then the basePackage scanned here is automatically obtained by it, which is actually the directory and subdirectory where the startup class is located. The following scanning process is similar to that of Method 1.


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

The MyBtatis team seems to prefer using @ Mapper, because they wrote in the comments on AutoConfiguredMapperScannerRegistrar that this method will scan the same basic pacakge as SpringBoot 1. If you want more power, you can use MapperScan annotations explicitly, but Mapper annotations make the type mapper work properly out of the box, just like using Spring Data JPA library 1.

/**
* 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.
*/

Reference article:

One thought on @ Mapper and @ MapperScan annotations of MyBatis

Creation of MapperFactoryBean

The Function and Difference of MapperFactoryBean and MapperScannerConfigurer


Related articles: