Thoroughly grasp the principle of Spring’s integration of MyBatis to completely conquer the interviewer, it is recommended to collect

At the end of the [MyBatis] article, we will give you a detailed introduction to how Spring integrates MyBatis. Let everyone thoroughly grasp the underlying design principles and implementation of MyBatis.

MyBatis integrates Spring principles

Integrating MyBatis into Spring is to further simplify the use of MyBatis, so it only encapsulates MyBatis and does not replace the core objects of MyBatis. [That is to say: SqlSessionFactory] , SqlSession, MapperProxy in the MyBatis jar package will be used. The classes in mybatis-spring.jar just do some packaging or bridge work.

As long as we understand how these three objects are created, we also understand the principle of Spring inheriting MyBatis. We divide it into three steps:

  1. Where is SqlSessionFactory created.
  2. Where is the SqlSession created.
  3. Where is the proxy class created.

1 SqlSessionFactory

First of all, let’s take a look at the creation process of SqlSessionFactory in MyBatis integration Spring, and check the entry of this step in the configuration integration tag in Spring’s configuration file.

We entered the SqlSessionFactoryBean to view the source code and found that it implements the three interfaces of InitializingBean, FactoryBean and ApplicationListener

interface method effect
FactoryBean getObject() Returns the Bean instance created by FactoryBean
InitializingBean afterPropertiesSet() Add operation after bean property initialization is complete
ApplicationListener onApplicationEvent() Monitor application time

1.1 afterPropertiesSet

Let’s first look at the logic in the afterPropertiesSet method

public void afterPropertiesSet() throws Exception {

    Assert.notNull(this.dataSource, "Property 'dataSource' is required");
    Assert.notNull(this.sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    Assert.state(this.configuration == null && this.configLocation == null || this.configuration == null || this.configLocation == null, "Property 'configuration' and 'configLocation' can not specified with together");
    this.sqlSessionFactory = this.buildSqlSessionFactory();
}

It can be found that the buildSqlSessionFactory method is directly called in the afterPropertiesSet to realize the creation of the sqlSessionFactory object

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

        // parse the XMLConfigBuilder object of the global configuration file
        XMLConfigBuilder xmlConfigBuilder = null ;
         // Configuration object
        Configuration targetConfiguration;
        Optional var10000;
        if ( this .configuration != null ) {
   // Determine whether there is a configuration object, if it exists, it means it has been parsed
            targetConfiguration = this.configuration;
            if (targetConfiguration.getVariables() == null) {

                targetConfiguration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {

                targetConfiguration.getVariables().putAll(this.configurationProperties);
            }
            // If the configuration object does not exist, but the configLocation attribute exists, build the xmlConfigBuilder object according to the file path of mybatis-config.xml
        } else if (this.configLocation != null) {

            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
            targetConfiguration = xmlConfigBuilder.getConfiguration();
        } else {

            // property 'configuration' or 'configLocation' not specified, use default MyBatis configuration
            LOGGER.debug(() -> {

                return "Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration";
            });
            targetConfiguration = new Configuration();
            var10000 = Optional.ofNullable(this.configurationProperties);
            Objects.requireNonNull(targetConfiguration);
            var10000.ifPresent(targetConfiguration::setVariables);
        }
        // Set the properties in Configuration, that is, we can set the settings in the global configuration file of MyBatis in the integration file of Mybatis and Spring
        var10000 = Optional.ofNullable(this.objectFactory);
        Objects.requireNonNull(targetConfiguration);
        var10000.ifPresent(targetConfiguration::setObjectFactory);
        var10000 = Optional.ofNullable(this.objectWrapperFactory);
        Objects.requireNonNull(targetConfiguration);
        var10000.ifPresent(targetConfiguration::setObjectWrapperFactory);
        var10000 = Optional.ofNullable(this.vfs);
        Objects.requireNonNull(targetConfiguration);
        var10000.ifPresent(targetConfiguration::setVfsImpl);
        Stream var24;
        if (StringUtils.hasLength(this.typeAliasesPackage)) {

            var24 = this.scanClasses(this.typeAliasesPackage, this.typeAliasesSuperType).stream().filter((clazz) -> {

                return !clazz.isAnonymousClass();
            }).filter((clazz) -> {

                return !clazz.isInterface();
            }).filter((clazz) -> {

                return !clazz.isMemberClass();
            });
            TypeAliasRegistry var10001 = targetConfiguration.getTypeAliasRegistry();
            Objects.requireNonNull(var10001);
            var24.forEach(var10001::registerAlias);
        }

        if (!ObjectUtils.isEmpty(this.typeAliases)) {

            Stream.of(this.typeAliases).forEach((typeAlias) -> {

                targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
                LOGGER.debug(() -> {

                    return "Registered type alias: '" + typeAlias + "'";
                });
            });
        }

        if (!ObjectUtils.isEmpty(this.plugins)) {

            Stream.of(this.plugins).forEach((plugin) -> {

                targetConfiguration.addInterceptor(plugin);
                LOGGER.debug(() -> {

                    return "Registered plugin: '" + plugin + "'";
                });
            });
        }

        if (StringUtils.hasLength(this.typeHandlersPackage)) {

            var24 = this.scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter((clazz) -> {

                return !clazz.isAnonymousClass();
            }).filter((clazz) -> {

                return !clazz.isInterface();
            }).filter((clazz) -> {

                return !Modifier.isAbstract(clazz.getModifiers());
            });
            TypeHandlerRegistry var25 = targetConfiguration.getTypeHandlerRegistry();
            Objects.requireNonNull(var25);
            var24.forEach(var25::register);
        }

        if (!ObjectUtils.isEmpty(this.typeHandlers)) {

            Stream.of(this.typeHandlers).forEach((typeHandler) -> {

                targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
                LOGGER.debug(() -> {

                    return "Registered type handler: '" + typeHandler + "'";
                });
            });
        }

        if (!ObjectUtils.isEmpty(this.scriptingLanguageDrivers)) {

            Stream.of(this.scriptingLanguageDrivers).forEach((languageDriver) -> {

                targetConfiguration.getLanguageRegistry().register(languageDriver);
                LOGGER.debug(() -> {

                    return "Registered scripting language driver: '" + languageDriver + "'";
                });
            });
        }

        var10000 = Optional.ofNullable(this.defaultScriptingLanguageDriver);
        Objects.requireNonNull(targetConfiguration);
        var10000.ifPresent(targetConfiguration::setDefaultScriptingLanguage);
        if (this.databaseIdProvider != null) {

            try {

                targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
            } catch (SQLException var23) {

                throw new NestedIOException("Failed getting a databaseId", var23);
            }
        }

        var10000 = Optional.ofNullable(this.cache);
        Objects.requireNonNull(targetConfiguration);
        var10000.ifPresent(targetConfiguration::addCache); // If the cache is not empty, add the cache to the configuration object
         if (xmlConfigBuilder != null ) {

            try {

                xmlConfigBuilder.parse(); // Parse the global configuration file
                LOGGER.debug(() -> {

                    return "Parsed configuration file: '" + this.configLocation + "'";
                });
            } catch (Exception var21) {

                throw new NestedIOException("Failed to parse config resource: " + this.configLocation, var21);
            } finally {

                ErrorContext.instance().reset();
            }
        }

        targetConfiguration.setEnvironment(new Environment(this.environment, (TransactionFactory)(this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory), this.dataSource));
        if (this.mapperLocations != null) {

            if (this.mapperLocations.length == 0) {

                LOGGER.warn(() -> {

                    return "Property 'mapperLocations' was specified but matching resources are not found.";
                });
            } else {

                Resource[] var3 = this.mapperLocations;
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {

                    Resource mapperLocation = var3[var5];
                    if (mapperLocation != null) {

                        try {

                            // Create an XMLMapperBuilder for parsing Mapper.xml and call its parse() method. We have learned about this step before.
                             // Mainly do two things, one is to register the addition, deletion, modification, and checking tags as a MappedStatement object.
                            // The second is to register the interface and the corresponding MapperProxyFactory factory class in the MapperRegistry
                            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                            xmlMapperBuilder.parse();
                        } catch (Exception var19) {

                            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", var19);
                        } finally {

                            ErrorContext.instance().reset();
                        }

                        LOGGER.debug(() -> {

                            return "Parsed mapper file: '" + mapperLocation + "'";
                        });
                    }
                }
            }
        } else {

            LOGGER.debug(() -> {

                return "Property 'mapperLocations' was not specified.";
            });
        }
      // Finally calling sqlSessionFactoryBuilder.build() returns a DefaultSqlSessionFactory.
        return  this .sqlSessionFactoryBuilder.build(targetConfiguration);
    }

In the afterPropertiesSet method, the creation of the SqlSessionFactory object has been completed, and the parsing operations of the related configuration files and mapping files have been completed.

Method summary: By defining a SqlSessionFactoryBean class that implements the InitializingBean interface, there is an afterPropertiesSet() method that will be called when the property values ​​of the bean are set. When Spring starts and initializes the bean, it completes the parsing and the creation of the factory class.

1.2 getObject

In addition, SqlSessionFactoryBean implements the FactoryBean interface.

The role of FactoryBean is to allow users to customize the logic of instantiating beans. If a Bean is obtained from the BeanFactory according to the Bean’s ID, it actually obtains the object returned by the FactoryBean’s getObject().

That is to say, when we get the SqlSessionFactoryBean, its getObject() method will be called.

public SqlSessionFactory getObject() throws Exception {

        if (this.sqlSessionFactory == null) {

            this.afterPropertiesSet();
        }

        return this.sqlSessionFactory;
    }

The logic in the getObject method is very simple. It returns the SqlSessionFactory object. If the SqlSessionFactory object is empty, it will call afterPropertiesSet again to parse and create it again.

1.3 onApplicationEvent

Implementing the ApplicationListener interface gives the SqlSessionFactoryBean the ability to monitor some event notifications issued by the application. For example, the ContextRefreshedEvent (context refresh event) is monitored here, which will be executed after the Spring container is loaded. What is done here is to check if ms is loaded.

public void onApplicationEvent(ApplicationEvent event) {

    if (this.failFast && event instanceof ContextRefreshedEvent) {

        this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
    }
}

2 SqlSession

2.1 Problems with DefaultSqlSession

In the previous introduction to the use of MyBatis, the DefaultSqlSession is obtained through the open method of SqlSessionFactory, but in Spring we cannot use DefaultSqlSession directly, because DefaultSqlSession is thread-unsafe. Therefore, there will be data security problems in direct use. For this problem, a corresponding tool SqlSessionTemplate is provided in the integrated MyBatis-Spring plug-in package.

https://mybatis.org/mybatis-3/zh/getting-started.html

That is, when we use SqlSession, we need to use try catch block to handle it

try (SqlSession session = sqlSessionFactory.openSession()) {

  // your application logic code
}
// or 
SqlSession session = null ;
 try {

   session = sqlSessionFactory.openSession();
  // your application logic code 
} finally {

    session.close();
}

In the integration of Spring, the operation is simplified by the provided SqlSessionTemplate, which provides security processing.

2.2 SqlSessionTemplate

In the mybatis-spring package, a thread-safe SqlSession wrapper class is provided to replace SqlSession. This class is SqlSessionTemplate. Because it is thread-safe, an instance can be shared among all DAO layers (singleton by default).

[External link image transfer failed, the origin site may have anti-leech mechanism, it is recommended to save the image and upload it directly (img-AYxyfLIh-1622729398252)(https://img2.tuicool.com/rQfQFvV.png!web)]

Although SqlSessionTemplate defines all methods of manipulating data, such as selectOne(), selectList(), insert(), update(), delete(), etc., like DefaultSqlSession, it does not have its own implementation, and all calls the method of a proxy object.

So how did SqlSessionProxy come about? There is an answer in the constructor of SqlSessionTemplate

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;
       // Create a proxy object of SqlSession interface and call the selectOne() method in SqlSessionTemplate, which is actually calling 
      // the selectOne() method of SqlSessionProxy, Then execute the invoke method in SqlSessionInterceptor 
    this .sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
         new Class[] {
  SqlSession.class }, new SqlSessionInterceptor());
  }

Through the above introduction, then we should enter the invoke method of SqlSessionInterceptor.

Although the above code looks complicated, it is essentially the following operation

SqlSession session = null;
try {

   session = sqlSessionFactory.openSession();
  // your application logic code 
} finally {

    session.close();
}

The key code in the getSqlSession method:

Implementation process

To sum up: because DefaultSqlSession itself cannot generate a new instance for each request call, we simply create a proxy class, also implement SqlSession, provide the same method as DefaultSqlSession, and create a DefaultSqlSession first when any method is called. instance, and then call the corresponding method of the proxied object.

MyBatis also comes with a thread-safe SqlSession implementation: SqlSessionManager, which is implemented in the same way. If it is not integrated into Spring to ensure thread safety, SqlSessionManager is used.

2.3 SqlSessionDaoSupport

Through the above introduction, we know that in the Spring project, we should use SqlSessionTemplate to perform database operations, then we should first add SqlSessionTemplate to the IoC container, and then we use @Autowired in Dao to get the specific steps. Refer to the official website: http:/ /mybatis.org/spring/en/sqlsession.html

Then we can look at the code in SqlSessionDaoSupport

In this way, in the Dao layer, we only need to inherit SqlSessionDaoSupport to operate directly through the getSqlSession method.

public abstract class SqlSessionDaoSupport extends DaoSupport {


 private SqlSessionTemplate sqlSessionTemplate;

 public SqlSession getSqlSession() {

   return this.sqlSessionTemplate;
}
//Other code omitted

That is to say, we let the DAO layer (implementation class) inherit the abstract class SqlSessionDaoSupport, and automatically have the getSqlSession() method. Call getSqlSession() to get the shared SqlSessionTemplate.

The SQL format executed at the DAO layer is as follows:

getSqlSession().selectOne(statement, parameter);
getSqlSession().insert(statement);
getSqlSession().update(statement);
getSqlSession().delete(statement);

Still not concise enough. In order to reduce repetitive code, we usually do not let our implementation class directly inherit SqlSessionDaoSupport, but first create a BaseDao to inherit SqlSessionDaoSupport. The operations on the database are encapsulated in BaseDao, including methods such as selectOne(), selectList(), insert(), and delete(), and subclasses can call them directly.

public  class BaseDao extends SqlSessionDaoSupport {

   @Autowired
   private SqlSessionFactory sqlSessionFactory;

   @Autowired
   public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {

       super.setSqlSessionFactory(sqlSessionFactory);
  }

   public Object selectOne(String statement, Object parameter) {

       return getSqlSession().selectOne(statement, parameter);
  }
// omitted later

Then let our DAO layer implementation class inherit BaseDao and implement our Mapper interface. The implementation class needs to be annotated with @Repository.

In the method of the implementation class, we can directly call the selectOne() method encapsulated by the parent class (BaseDao), then it will eventually call the selectOne() method of sqlSessionTemplate.

@Repository
public class EmployeeDaoImpl extends BaseDao implements EmployeeMapper {

   @Override
   public Employee selectByPrimaryKey(Integer empId) {

       Employee emp = (Employee) this.selectOne("com.gupaoedu.crud.dao.EmployeeMapper.selectByPrimaryKey",empId);
       return emp;
  }
// omitted later

Then in the place where it needs to be used, such as the Service layer, inject our implementation class and call the method of the implementation class. Here we directly inject it in the unit test class DaoSupportTest.java:

@Autowired
EmployeeDaoImpl employeeDao;

@Test
public void EmployeeDaoSupportTest() {

   System.out.println(employeeDao.selectByPrimaryKey(1));
}

Finally, the method of DefaultSqlSession will be called.

2.4 MapperScannerConfigurer

We introduced SqlSessionTemplate and SqlSessionDaoSupport above, and we also understand their functions, but when we actually develop, we can still directly obtain the proxy object of Mapper, and we have not created the implementation class of Mapper. How is this implemented? This we have to pay attention to in the configuration file that integrates MyBatis, in addition to SqlSessionFactoryBean, we also set up a MapperScannerConfigurer, let’s analyze this class

By implementing this interface, you can modify the definition of some beans in the container before Spring creates them. This method is called before Spring creates the bean.

@Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {

    if (this.processPropertyPlaceHolders) {

      processPropertyPlaceHolders(); // process placeholders
    }

    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));
    }
      // Generate the corresponding filter according to the above configuration
    scanner.registerFilters();
      // Start scanning the package and its subpackages specified in the basePackage field
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

The core of the above code is the scan method

public int scan(String... basePackages) {

    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    this.doScan(basePackages);
    if (this.includeAnnotationConfig) {

        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

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

Then the doScan method in the subclass ClassPathMapperScanner will be called

@Override 
  public Set<BeanDefinitionHolder> doScan (String... basePackages)  {

      // Call the doScan method in the parent class to scan all the interfaces and add all the interfaces to beanDefinitions. 
    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 {

        // When registering beanDefinitions, BeanClass is changed to MapperFactoryBean
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

Because an interface cannot create an instance object, then we point the interface type to a specific common Java type, MapperFactoryBean , before creating the object. That is to say, all Mapper interfaces are registered in the container as A MapperFactoryBean that supports generics. Then the MapperFactoryBean object is created when the object of this interface is created.

2.5 MapperFactoryBean

Why sign up for it? When injection is used, it is also this object. What is the function of this object? First, let’s take a look at their class diagram structure

From the class diagram, we can see that MapperFactoryBean inherits SqlSessionDaoSupport, so every place where Mapper is injected can get the SqlSessionTemplate object. Then we also found that MapperFactoryBean implements the FactoryBean interface, which means that when injecting the MapperFactoryBean object into the container, it essentially injects the returned object of the getObject method into the container,

/**
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {

     // From this, we can see that in essence, the Mapper interface still obtains a JDBC proxy object through the DefaultSqlSession.getMapper method, which is associated with what we explained earlier. 
    return getSqlSession().getMapper( this .mapperInterface);
  }

It doesn’t directly return a MapperFactoryBean. Instead, the getMapper() method of SqlSessionTemplate is called. The essence of SqlSessionTemplate is a proxy, so it will eventually call the getMapper() method of DefaultSqlSession. We will not repeat the following process. In other words, the final return is a JDK dynamic proxy object.

So the last call to any method of the Mapper interface is also to execute the invoke() method of MapperProxy, and the subsequent process is exactly the same as that of the programming project.

To sum up, how did Spring inherit MyBatis?

  1. Provides a substitute for SqlSession SqlSessionTemplate, which has an internal SqlSessionInterceptor that implements InvocationHandler, which is essentially a proxy for SqlSession.
  2. Provides an abstract class SqlSessionDaoSupport for obtaining SqlSessionTemplate.
  3. Scan the Mapper interface and register it in the container is MapperFactoryBean, which inherits SqlSessionDaoSupport and can obtain SqlSessionTemplate.
  4. When injecting Mapper into use, it calls the getObject() method, which actually calls the getMapper() method of SqlSessionTemplate and injects a JDK dynamic proxy object.
  5. Execute any method of the Mapper interface, it will go to the trigger management class MapperProxy and enter the SQL processing flow.

Core object:

object The life cycle
SqlSessionTemplate An alternative to SqlSession in Spring that is thread-safe
SqlSessionDaoSupport Used to get SqlSessionTemplate
SqlSessionInterceptor (inner class) Proxy object, used to proxy DefaultSqlSession, used in SqlSessionTemplate
MapperFactoryBean Proxy object, inherits SqlSessionDaoSupport to get SqlSessionTemplate
SqlSessionHolder Controlling SqlSession and Transactions

Finally, let’s summarize the relevant design patterns used in the source code of MyBatis:

Design Patterns kind
factory pattern SqlSessionFactory、ObjectFactory、MapperProxyFactory
builder mode XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuidler
singleton pattern SqlSessionFactory、Configuration、ErrorContext
proxy mode Binding: MapperProxy Lazy Loading: ProxyFactory Plugin: PluginSpring Integration MyBaits: SqlSessionTemplate’s internal SqlSessionInterceptorMyBatis own connection pool: PooledConnection Log printing: ConnectionLogger, StatementLogger
adapter mode Log, for log components such as Log4j and JDK logging that do not directly implement the slf4j interface, an adapter is required
Template method BaseExecutor、SimpleExecutor、BatchExecutor、ReuseExecutor
Decorator pattern LoggingCache, LruCache to PerpetualCacheCachingExecutor to other Executors
Chain of Responsibility Model Interceptor、InterceptorChain

~~ Well, the principle analysis of Spring’s integration of MyBatis will be introduced here. If it is helpful to you, please like, follow and add to favorites

Leave a Comment

Your email address will not be published. Required fields are marked *