Thoroughly master the principles of Spring integration with MyBatis and thoroughly 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 master the underlying design principles and implementation of MyBatis.

Insert picture description here

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. In other words: 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 the SqlSessionFactory created?
  2. Where is the SqlSession created?
  3. Where is the proxy class created?

1 SqlSessionFactory

First of all, let's look at the creation process of SqlSessionFactory in MyBatis integrating Spring. Check the entry of this step. In the configuration integration tag in the Spring configuration file,

Insert picture description here


we enter the SqlSessionFactoryBean to view the source code and find that it implements the InitializingBean, FactoryBean, and ApplicationListener three interfaces

interfacemethodeffect
FactoryBeangetObject()Returns the Bean instance created by FactoryBean
InitializingBeanafterPropertiesSet()Add operation after bean property initialization is complete
ApplicationListeneronApplicationEvent()Monitor the 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 {
        // 解析全局配置文件的 XMLConfigBuilder 对象
        XMLConfigBuilder xmlConfigBuilder = null;
        // Configuration 对象
        Configuration targetConfiguration;
        Optional var10000;
        if (this.configuration != null) { // 判断是否存在 configuration对象,如果存在说明已经解析过了
            targetConfiguration = this.configuration;
            // 覆盖属性
            if (targetConfiguration.getVariables() == null) {
                targetConfiguration.setVariables(this.configurationProperties);
            } else if (this.configurationProperties != null) {
                targetConfiguration.getVariables().putAll(this.configurationProperties);
            }
            // 如果configuration对象不存在,但是存在configLocation属性,就根据mybatis-config.xml的文件路径来构建 xmlConfigBuilder对象
        } else if (this.configLocation != null) {  
            xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
            targetConfiguration = xmlConfigBuilder.getConfiguration();
        } else {
            // 属性'configuration'或'configLocation'未指定,使用默认MyBatis配置
            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);
        }
		// 设置 Configuration 中的属性  即我们可以在Mybatis和Spring的整合文件中来设置 MyBatis的全局配置文件中的设置
        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); // 如果cache不为空就把cache 添加到 configuration对象中
        if (xmlConfigBuilder != null) {
            try {
                xmlConfigBuilder.parse(); // 解析全局配置文件
                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 {
                            //创建了一个用来解析Mapper.xml的XMLMapperBuilder,调用了它的parse()方法。这个步骤我们之前了解过了,
                            //主要做了两件事情,一个是把增删改查标签注册成MappedStatement对象。
                            // 第二个是把接口和对应的MapperProxyFactory工厂类注册到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.";
            });
        }
      // 最后调用sqlSessionFactoryBuilder.build()返回了一个DefaultSqlSessionFactory。
        return this.sqlSessionFactoryBuilder.build(targetConfiguration);
    }

​ The creation of the SqlSessionFactory object is completed in the afterPropertiesSet method, and the relevant configuration files and mapping files have been parsed.

Method summary: by defining a SqlSessionFactoryBean class that implements the InitializingBean interface, there is an afterPropertiesSet() method that will be called when the bean property values ​​are set. When Spring initializes this Bean, it completes the analysis and 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 the Bean. If you get a Bean from the BeanFactory based on the ID of the Bean, what it gets is actually the object returned by FactoryBean's getObject().

In other words, when we get the SqlSessionFactoryBean, we will call its getObject() method.

    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, afterPropertiesSet is called again to parse and create it.

1.3 onApplicationEvent

Implementing the ApplicationListener interface gives SqlSessionFactoryBean the ability to monitor some event notifications sent by the application. For example, the ContextRefreshedEvent is monitored here, and it will be executed after the Spring container is loaded. What is done here is to check whether 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 of the use of MyBatis, DefaultSqlSession is obtained through the open method of SqlSessionFactory, but in Spring we cannot directly use DefaultSqlSession, because DefaultSqlSession is not thread-safe. Therefore, there will be data security issues in direct use. For this problem, a corresponding tool SqlSessionTemplate is provided in the integrated MyBatis-Spring plug-in package.

Insert picture description here


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

Insert picture description here

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

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}
// 或者
SqlSession session = null;
try {
   session = sqlSessionFactory.openSession();
  // 你的应用逻辑代码
}finally{
    session.close();
}

In the integration of Spring, the operation is simplified through the provided SqlSessionTemplate and safe processing is provided.

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, it can share an instance in all DAO layers (singleton by default).

Insert picture description here


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

Insert picture description here

So how does SqlSessionProxy come from? There is an answer in the construction method 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;
      // 创建了一个 SqlSession 接口的代理对象, 调用SqlSessionTemplate中的 selectOne() 方法,其实就是调用
      // SqlSessionProxy的 selectOne() 方法,然后执行的是 SqlSessionInterceptor里面的 invoke方法
    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.

Insert picture description here

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

SqlSession session = null;
try {
   session = sqlSessionFactory.openSession();
  // 你的应用逻辑代码
}finally{
    session.close();
}

The key code in the getSqlSession method:

Insert picture description here

The execution flow is

Insert picture description here


summarized : Because DefaultSqlSession itself cannot generate a new instance every time a request is called, we simply create a proxy class and implement SqlSession, providing the same method as DefaultSqlSession, which is created first when any method is called A DefaultSqlSession instance, and then call the corresponding method of the proxy 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, use SqlSessionManager.

2.3 SqlSessionDaoSupport

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

Insert picture description here

Then we can look at the code in SqlSessionDaoSupport

Insert picture description here

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

public abstract class SqlSessionDaoSupport extends DaoSupport {

 private SqlSessionTemplate sqlSessionTemplate;

 public SqlSession getSqlSession() {
   return this.sqlSessionTemplate;
}
// 其他代码省略

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

The SQL format executed in 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 the repetitive code, we usually do not let our implementation class directly inherit SqlSessionDaoSupport, but first create a BaseDao to inherit SqlSessionDaoSupport. Encapsulate database operations in BaseDao, including methods such as selectOne(), selectList(), insert(), and delete(), which can be directly called by subclasses.

public  class BaseDao extends SqlSessionDaoSupport {
   //使用sqlSessionFactory
   @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);
  }
// 后面省略

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;
  }
// 后面省略

Then in the place that 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 the unit test class DaoSupportTest.java:

@Autowired
EmployeeDaoImpl employeeDao;

@Test
public void EmployeeDaoSupportTest() {
   System.out.println(employeeDao.selectByPrimaryKey(1));
}

Eventually, the method of DefaultSqlSession will be called.

2.4 FoldersScannerConfigure

We have introduced SqlSessionTemplate and SqlSessionDaoSupport above, and we are clear about their roles, but when we actually develop, we can still directly obtain the proxy object of the Mapper without creating the implementation class of the Mapper. How is this implemented? For this, we must pay attention to the integration of MyBatis configuration file in addition to SqlSessionFactoryBean, we also set up a MapperScannerConfigurer, let’s analyze this class

The first is that the inheritance structure of

Insert picture description here


MapperScannerConfigurer, MapperScannerConfigurer, implements the BeanDefinitionRegistryPostProcessor interface. BeanDefinitionRegistryPostProcessor is a subclass of BeanFactoryPostProcessor, which has a postProcessBeanDefinitionRegistry() method.

After implementing this interface, you can modify the definition of some Bean in the container before Spring creates the Bean. Spring will call this method before creating the Bean.

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
      processPropertyPlaceHolders(); // 处理 占位符
    }

      // 创建 ClassPathMapperScanner 对象
    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));
    }
      // 根据上面的配置生成对应的 过滤器
    scanner.registerFilters();
      // 开始扫描basePackage字段中指定的包及其子包
    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) {
      // 调用父类中的 doScan方法 扫描所有的接口,把接口全部添加到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 {
        // 在注册beanDefinitions的时候,BeanClass被改为MapperFactoryBean
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }
Insert picture description here

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

2.5 MapperFactoryBean

Why register as it? When it is injected and used, it is also this object. What is the function of this object? First look at their class diagram structure

Insert picture description here

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

  /**
   * {@inheritDoc}
   */
  @Override
  public T getObject() throws Exception {
     // 从这可以看到 本质上 Mapper接口 还是通过DefaultSqlSession.getMapper方法获取了一个JDBC的代理对象,和我们前面讲解的就关联起来了
    return getSqlSession().getMapper(this.mapperInterface);
  }

It does not 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, what is returned in the end is a dynamic proxy object of the JDK.

So in the end, any method that calls the Mapper interface is also to execute the invoke() method of MapperProxy, and the subsequent process is exactly the same as in the programmatic project.

To sum up , how did Spring inherit MyBatis?

SqlSessionTemplate is provided as an alternative to SqlSession. There is an internal SqlSessionInterceptor that implements InvocationHandler, which is essentially a proxy for SqlSession.

Provides the abstract class SqlSessionDaoSupport for obtaining SqlSessionTemplate.

Scanning the Mapper interface, the one registered in the container is MapperFactoryBean, which inherits SqlSessionDaoSupport and can obtain SqlSessionTemplate.

When injecting Mapper into use, the getObject() method is called, which actually calls the getMapper() method of SqlSessionTemplate and injects a JDK dynamic proxy object.

Executing any method of the Mapper interface will go to the trigger management class MapperProxy and enter the SQL processing flow.

Core object:

ObjectLife cycle
SqlSessionTemplateAlternative to SqlSession in Spring, which is thread-safe
SqlSessionDaoSupportUsed to get SqlSessionTemplate
SqlSessionInterceptor (internal class)Proxy object, used to proxy DefaultSqlSession, used in SqlSessionTemplate
MapperFactoryBeanProxy object, inherited SqlSessionDaoSupport to obtain SqlSessionTemplate
SqlSessionHolderControl SqlSession and transactions

Finally, I will summarize the relevant design patterns used in the MyBatis source code:

Design Patternsclass
Factory modeSqlSessionFactory、ObjectFactory、MapperProxyFactory
Builder modeXMLConfigBuilder、XMLMapperBuilder、XMLStatementBuidler
Singleton modeSqlSessionFactory、Configuration、ErrorContext
Agency modelBinding: MapperProxy
lazy loading: ProxyFactory
plug-in: PluginSpring
integration MyBaits: SqlSessionTemplate's internal SqlSessionInterceptor
MyBatis comes with connection pool: PooledConnection
log printing: ConnectionLogger, StatementLogger
Adapter modeLog, for Log4j, JDK logging, which do not directly implement the slf4j interface, an adapter is required
Template methodBaseExecutor、SimpleExecutor、BatchExecutor、ReuseExecutor
Decorator modeLoggingCache, LruCache to PerpetualCache, CachingExecutor to other Executors
Chain of Responsibility ModelInterceptor、InterceptorChain

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