Analysis on the principle of MyBatis integrating Spring

Catalog

If not combined with the Spring framework, a typical way to use MyBatis is as follows:

public class UserDaoTest {

    private SqlSessionFactory sqlSessionFactory;

    @Before
    public void setUp() throws Exception{
        ClassPathResource resource = new ClassPathResource("mybatis-config.xml");
        InputStream inputStream = resource.getInputStream();
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void selectUserTest(){
        String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
        SqlSession sqlSession = sqlSessionFactory.openSession();
        CbondissuerMapper cbondissuerMapper = sqlSession.getMapper(CbondissuerMapper.class);
        Cbondissuer cbondissuer = cbondissuerMapper.selectByPrimaryKey(id);
        System.out.println(cbondissuer);
        sqlSession.close();
    }

}

We need SqlSessionFactory first, and then obtain SqlSession through the openSession method of SqlSessionFactory. Get the dynamic proxy class (MapperProxy) of the interface we defined through SqlSession. When we integrate the Spring framework, we use MyBatis in a simple "unimaginable" way:

@Autowore
private CbondissuerMapper cbondissuerMapper;

It's very Spring like. Just inject it directly. How is this realized? How does Spring omit the slightly complicated template code above? My first instinct is that Spring did the initialization of Mybatis when it started, and then got all the dynamic proxy implementation classes of Mapper interface at one time and put them into the Spring container for management.

Now let's test your conjecture.

Analysis on the principle of MyBatis integrating Spring

The following is a simple analysis based on mybatis autoconfiguration in Spring Boot:

  //The final function of SqlSessionFactoryBean is to parse MyBati configuration file, generate configuration object, generate DefaultSqlSessionFactory, and join Spring's container management. It can be seen that SqlSessionFactoryBean has the same function as sqlsessionfactorbeanbuilder.
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    Configuration configuration = this.properties.getConfiguration();
    if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
      configuration = new Configuration();
    }
    if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
      for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
        customizer.customize(configuration);
      }
    }
    factory.setConfiguration(configuration);
    if (this.properties.getConfigurationProperties() != null) {
      factory.setConfigurationProperties(this.properties.getConfigurationProperties());
    }
    if (!ObjectUtils.isEmpty(this.interceptors)) {
      factory.setPlugins(this.interceptors);
    }
    if (this.databaseIdProvider != null) {
      factory.setDatabaseIdProvider(this.databaseIdProvider);
    }
    if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
      factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }

    return factory.getObject();
  }

  //Create the Bean SqlSessionTemplate, which dynamically proxies DefaultSqlSession, so the DefaultSqlSession is called finally. Next, we will focus on the analysis of SqlSessionTemplate
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

The following is the source code of SqlSessionTemplate. Due to the large number of source code, only the key parts are posted:

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  private final SqlSessionFactory sqlSessionFactory;

  private final ExecutorType executorType;
  //SqlSessionTemplate dynamically proxies DefaultSqlSession. All calls to sqlSessionProxy will pass through the proxy object
  private final SqlSession sqlSessionProxy;

  private final PersistenceExceptionTranslator exceptionTranslator;
    
  ....
  
  public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
  }
    
  ....
  //CRUD calls to sqlSessionProxy will be called here first
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      //This step is equivalent to calling the opensession method of sqlSessionFactory to obtain SqlSession
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
        //Call CRUD method of DefaultSqlSession
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          //Finally, force SqlSession to close
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
}

So far, the whole process of getting SqlSessionFactory, getting SqlSession, executing CRUD, and closing SqlSession in Spring has been analyzed. There is also a question about when the Mapper interface implementation classes are generated dynamically.

MapperScan's Secret

We know MapperScan is used to scan Mapper interface, so we naturally want to explore MapperScan.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
//The secret is in MapperScannerRegistrar
@Import(MapperScannerRegistrar.class)
public @interface MapperScan {

  String[] value() default {};
  //Set up scanned packages
  String[] basePackages() default {};

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

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

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

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

  String sqlSessionTemplateRef() default "";
    
  String sqlSessionFactoryRef() default "";
  //Specify a custom MapperFactoryBean
  Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

}

Here's what MapperScannerRegistrar does:

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
    //Create a ClassPathMapperScanner and set the properties according to the configuration in @ MapperScan
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

    // this check is needed in Spring 3.1
    if (resourceLoader != null) {
      scanner.setResourceLoader(resourceLoader);
    }

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

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
      scanner.setMarkerInterface(markerInterface);
    }

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

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
      scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass));
    }

    scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef"));
    scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef"));

    List<String> basePackages = new ArrayList<String>();
    for (String pkg : annoAttrs.getStringArray("value")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (String pkg : annoAttrs.getStringArray("basePackages")) {
      if (StringUtils.hasText(pkg)) {
        basePackages.add(pkg);
      }
    }
    for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) {
      basePackages.add(ClassUtils.getPackageName(clazz));
    }
    scanner.registerFilters();
    //Start scanning
    scanner.doScan(StringUtils.toStringArray(basePackages));
  }

The above creates a ClassPathMapperScanner, and sets properties according to the configuration in @ MapperScan to start scanning. Let's look at ClassPathMapperScanner.

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    GenericBeanDefinition definition;
    //Further processing of the scanned BeanDefinition
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (GenericBeanDefinition) holder.getBeanDefinition();

      if (logger.isDebugEnabled()) {
        logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() 
          + "' and '" + definition.getBeanClassName() + "' mapperInterface");
      }
 definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
      //Here is the key point. Set the bean class of the Mapper interface scanned to MapperFactoryBrean
      //Mapperfactorybean is a factory Bean used to generate MapperProxy;
      //When Spring automatically injects Mapper, it will automatically call the getObject method of the factory Bean, generate MapperProxy and put it into the Spring container.
      definition.setBeanClass(this.mapperFactoryBean.getClass());

      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) {
        if (logger.isDebugEnabled()) {
          logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        }
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }
    }
  }

The process of Spring automatically generating Mapper interface implementation class is also analyzed.

Brief summary

Through the above analysis, it confirms the conjecture at the beginning: Spring did the initialization of Mybatis when it started, and then obtained all the dynamic proxy implementation classes of Mapper interface at one time and put them into the Spring container for management. The general process is as follows:

  • Configure SqlSessionFactoryBean. The final function of this Bean is to create DefaultSqlSessionFactory and add DefaultSqlSessionFactory to Spring container management;
  • Create SqlSessionTemplate, which represents DefaultSqlSession in MyBatis. Calling any CRUD method through SqlSessionTemplate will experience openSession, calling CRUD method of DefaultSqlSession, and closing Session;

The process of Mapper interface generation is as follows:

  • @MapperScan annotation scans all Mapper interfaces to generate corresponding BeanDefinition;
  • ClassPathMapperScanner's processing method further configures these beandefinitions. The final step is to set the Beanclass property of these beandefinitions to MapperFactoryBean (this is a factory Bean, which is specially used to generate Mapper's dynamic proxy implementation MapperProxy, and a Dao interface will correspond to a MapperFactoryBean);
  • When Spring detects that Mapper needs to be injected automatically, it will call the getObject method of MapperFactoryBean to generate the corresponding MapperProxy and bring the object into Spring management.

The above is about the integration of mybatis into Spring. We found that its essence is the same as the traditional use of mybatis, except for some customized configuration through Spring.

The analysis is a bit messy. Let's do it first~

Keywords: Java Spring Mybatis xml Session

Added by reckdan on Wed, 08 Apr 2020 16:03:56 +0300