Mybatis-Spring Source Analysis

Posted by lice200 on Wed, 20 Nov 2019 08:01:31 +0100

Analyzing how Mybatis integrates into the framework using Spring's extension points, Mybatis's own extension points are no longer covered in this analysis

Build environment

Upload Github https://github.com/mybatis/spring .After several unsuccessful attempts through Git, the zip package was directly downloaded and imported into Idea.

The import process will be a bit slow, and a lot of things will be downloaded.Be sure to modify Maven's configuration file and local warehouse address, otherwise packages you've previously downloaded will be downloaded to the local warehouse on drive C

Test Code

Create a new directory directly under the source directory to write test code

Test Class


@Configuration
@MapperScan("com.jv.mapper")
@ComponentScan("com.jv.scan")
public class TestMybatis {

  public static void main(String[] args) {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestMybatis.class);
    UserService bean = ac.getBean(UserService.class);
    System.out.println(bean.query());
  }

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    PooledDataSource dataSource = new PooledDataSource();
    dataSource.setDriver("org.mariadb.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://192.168.10.12:3306/acct?useSSL=false&serverTimezone=UTC");
    dataSource.setUsername("dev01");
    dataSource.setPassword("12340101");
    factoryBean.setDataSource(dataSource);
    return factoryBean.getObject();
  }
}

Service class

@Service
public class UserService {
  @Autowired
  private UserMapper userMapper;

  public List<User> query(){
    return userMapper.query();
  }
}

Mapper class

public interface UserMapper {
  @Select("SELECT name,age FROM user")
  List<User> query();
}

Entity Class

@ToString
public class User {

  private String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Integer getAge() {
    return age;
  }

  public void setAge(Integer age) {
    this.age = age;
  }

  private Integer age;
}

Note: Always modify pom.xml before running.Because Spring-related dependencies imported by Mybatis do not take effect at runtime

<scope>provided</scope>comment out all, otherwise run better than class

Table structure:

CREATE TABLE `user` (
  `name` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  `age` int(4) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

insert into user(name,age) values("Messi",35);

commit;

Source Code Analysis

The use of @MapperScan can be found in the official mybatis-spring documentation: http://mybatis.org/spring/mappers.html

Register BeanDefinition

Now that integration with Spring is done through @MapperScan, start with it

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

Where @Import(MapperScannerRegistrar.class) is the focus, look at MapperScannerRegistrar

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

  /**
   * {@inheritDoc}
   * 
   * @deprecated Since 2.0.2, this method not used never.
   */
  @Override
  @Deprecated
  public void setResourceLoader(ResourceLoader resourceLoader) {
    // NOP
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    AnnotationAttributes mapperScanAttrs = AnnotationAttributes
        .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    if (mapperScanAttrs != null) {
      registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
    }
  }

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

    /**
     * Register a BeanDefinition for MapperScannerConfigurer, which implements BeanDefinitionRegistryPostProcessor
     * BeanDefinitionRegistryPostProcessor The implementation class of the interface, once placed in the Spring container, can act as a function of registering the BeanDefinition it needs when the Spring container starts
     */

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

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

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

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

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

    //Generate base package paths for all scans to scan
    List<String> basePackages = new ArrayList<>();
    basePackages.addAll(
        Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));

    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()));

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

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

    //Register BeanDefinition for MapperScannerConfigurer
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

  }

}

MapperScannerRegistrar implements ImportBeanDefinitionRegistrar

This is the key point for its integration into Spring, which you can refer to for ImportBeanDefinitionRegistrar https://my.oschina.net/u/3049601/blog/3129295

MapperScannerRegistrar completes the external import of BeanDefinition corresponding to the MapperScannerConfigurer class when the Spring container is initialized

MapperScannerConfigurator implementations BeanDefinitionRegistryPostProcessor (another extension of Spring and also extends BeanDefinition registration functionality), but it takes effect later than ImportBeanDefinitionRegistrar because the former triggers when Spring's ConfigurationClassPostProcessor Implements PriorityOrder.This class is the key class for all Mappers in the scan path

/**
 * BeanDefinitionRegistryPostProcessor Recursively search for interfaces from the base package and register them as MapperFactoryBean s
 *   MapperFactoryBean Importantly, it implements InitializingBean, Spring makes its abstract method afterPropertiesSet call after the Bean property is set to complete some initialization
 */
public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {

  .................Omit some code..................
 
  @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);
    //The default is MapperFactoryBean, which can be customized
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    scanner.registerFilters();
    //Start Scanning
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

  .................Omit some code..................

}

ClassPathMapperScanner inherits from Spring.ClassPathBeanDefinitionScanner and overrides the doScan method


public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
  .................Omit some code..................

  /**
   * Overrides the doScan method of ClassPathBeanDefinitionScanner, but the scanning is done by the doScan of the parent class
   */
  @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;
  }

  /**
   * Overrides the processBeanDefinitions method of ClassPathBeanDefinitionScanner
   * Complete property filling for BeanDefinition
   * setAutowireMode=AbstractBeanDefinition.AUTOWIRE_BY_TYPE is the key to Spring's automatic injection based on type.
   * @param beanDefinitions
   */
  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();
      String beanClassName = definition.getBeanClassName();
      LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
          + "' mapperInterface");

      // the mapper interface is the original class of the bean
      // but, the actual class of the bean is MapperFactoryBean
      /**
       * The custom Mapper interface is only the initial Class of the Bean, and when Spring initializes, the Bean's lass is actually a MapperFactoryBean
       */
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
      //Set BeanClass to MapperFactoryBean
      definition.setBeanClass(this.mapperFactoryBeanClass);

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

      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) {
        LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        // Quite important, this is Spring's dependency injection based on its type.
        // When @Autowired is used, Spring is not automatically injected by default, that is, autowireMode is AUTOWIRE_NO
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
      definition.setLazyInit(lazyInitialization);
    }
  }

  .................Omit some code..................
}

The code above completes a class scan of the basePackage, and Mybatis enhances the BeanDefinition generated from the scan, with two important points:

1.BeanClass=MapperFactoryBean 

That is, after the initial Spring container initialization is complete, all Mappers are not actually instantiated (you can see if there are any objects in the XXXApplicationContext.beanFactory.FactoryBeanObjectCache), but rather their FactoryBean has already been instantiated.Create it when a Mapper is needed, or if it is a singleton, place the instantiated Bean in the FactoryBeanObjectCache

2.autowireMode=AUTOWIRE_BY_TYPE

Auto-injection by type is not directly related to @Autowired. Spring defaults to AUTOWIRE_NO, but Spring finds that you use the @Autowired annotation to automatically inject by type.Automatic injection by type must have a setXXX method.

Represents scanned classes that support instantiation by type. Why set this value?Our own Mapper is an interface, so why inject murmur into it?The root cause is MapperFactoryBean extends SqlSessionDaoSupport

There are two set methods in SqlSessionDaoSupport:

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory)

public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate)

You can verify that you commented out "definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)" with an error:

Represents no injection success and verifies the importance of AUTOWIRE_BY_TYPE

 

As of this point, the BeanDefinition (BeanClass=MapperFactoryBean) of the Mybatis related class has been fully registered and instantiated.

instantiation

There is a very important point to instantiate:

As mentioned earlier, MapperFactoryBean is the one that completes instantiation, not the real Mapper. Why?

Next, look at the class diagram for MapperFactoryBean

You can see that the InitializingBean is finally implemented, and Spring calls the afterPropertiesSet() method of the implementation class after completing the property filling for the Bean that implements the interface.

Let's see what the afterPropertiesSet() method does

    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        //The checkDaoConfig of MapperFactoryBean is called at runtime
        this.checkDaoConfig();

        try {
            this.initDao();
        } catch (Exception var2) {
            throw new BeanInitializationException("Initialization of DAO failed", var2);
        }
    }

MapperFactoryBean's checkDaoConfig method

  protected void checkDaoConfig() {
    super.checkDaoConfig();

    notNull(this.mapperInterface, "Property 'mapperInterface' is required");

    Configuration configuration = getSqlSession().getConfiguration();
    if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
      try {
        //very important
        configuration.addMapper(this.mapperInterface);
      } catch (Exception e) {
        logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
        throw new IllegalArgumentException(e);
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

The configuration.addMapper(this.mapperInterface) completes the MappedStatement addition.

configuration is a property in DefaultSqlSesstionFactory (default), which is globally unique

MappedStatement describes a SQL to execute, parameters, return types, and so on

With this step, everything you need to call the true query method is ready.

 

Summary: Mybatis uses @Import (class implements ImportBeanDefinitionRegistrar), BeanDefinitionRegistryPostProcessor, and InitializingBean extensions to complete the integration.

The following classes of Mybatis are important:

MapperScan
MapperScannerRegistrar
MapperScannerConfigurer
ClassPathMapperScanner
MapperFactoryBean
DefaultSqlSessionFactory
Configuration
MapperStatement

Topics: Programming Spring Mybatis github JDBC