Spring事务整合Mybatis的原理

311 阅读8分钟

上篇文章讲解了 Spring 事务的原理,这篇文章来分析下 Mybatis 是怎么整合进 Spring 的,以及 Mybatis 如何参与到 Spring 的事务管理。

1. Spring 整合 Mybatis 相关配置

接下来,我们将通过配置的方式,来说明 Spring 和 Mybatis 是如何配合工作的,有哪些组件参与了其中。

Mybatis-Spring 官方文档:mybatis.org/spring/zh_C…

通过官网文档可知,可以通过 Myabtis-Spring 将 Mybatis 整合进 Spring 环境,整合内容如下:

  • Mybatis 执行 CRUD 操作时,可以参与到 Spring 事务管理中;
  • 自动创建映射器 Mapper 和 SqlSession,并支持自动注入到其他 bean 中;

1.1. Mybatis-Spring 基础配置

maven 依赖如下:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.13</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.1.2</version>
</dependency>

Spring 环境(非 SpringBoot ) 整合 Mybatis 的 XML 配置如下:

<!-- 省略datasource配置 -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath:/mappers/**/*.xml" />
  <!-- ... -->
</bean>

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="换成你的mapper包" />
</bean>

注:SqlSessionFactoryBean 配置中 DataSource(数据源)是必传参数,这个数据源可以是任意数据源,比如 Druid。除此之外, SqlSessionFactoryBean 还支持配置其他参数,参考 SqlSessionFactoryBean 相关方法即可。

这 3 个组件的作用:

【① SqlSessionFactoryBean】

在纯 Myabits 环境中,我们借助 SqlSessionFactoryBuilder 来创建 SqlSessionFactory

在 MyBatis-Spring 中,通过配置 SqlSessionFactoryBean,Spring 将会自动创建 SqlSessionFactorySqlSessionFactoryBean通过 Spirng Bean 的生命周期回调完成对SqlSessionFactory的初始化。

【② SqlSessionTemplate】

SqlSessionTemplate 是线程安全版的 SqlSession 实现。

基本原理:对 SqlSession 接口进行动态代理,把 Mybatis 中的 SqlSession 实例与线程绑定,保证一个线程执行不同 mapper 时用到的 SqlSession 是同一个;

【③ MapperScannerConfigurer】

用于向 Spring 容器注册 @Mapper 接口对应的 FactoryBean,并支持自动注入到其他 bean;

基本原理:扫描 Mapper 接口,为 Mapper 接口在 Spring 容器里注册 BeanDefinition,由于没有实现类,所以设置 beanClass 为 MapperFactoryBean.class,以此支持 @Mapper 可以自动注入到其他 bean;

OK,到这里已经可以通过 Mybatis Mapper 进行 CRUD 操作了,但是还没有启用事务相关操作,接下来是 Spring 事务相关的配置。

1.2. 开启 Spring 事务相关配置

1.2.1. 配置 Spring 事务管理器

MyBatis-Spring 借助 Spring 的DataSourceTransactionManager来实现事务管理。相关配置如下:

<!-- 省略datasource配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <constructor-arg ref="dataSource" />
</bean>

注意:事务管理器的 DataSource 必须和创建 SqlSessionFactoryBean 的是同一个 DataSource ,否则事务管理器无法工作。

1.2.2. 开启 Spring 声明式事务(@Transactional

XML 配置如下:

<tx:annotation-driven transaction-manager="transactionManager"/>

1.3. 小结

完整 XML 配置如下:

<!-- 配置SqlSessionFactory 用于生产SqlSession实例 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath:/mappers/**/*.xml" />
  <!-- ... -->
</bean>

<!-- 配置SqlSessionTemplate (单例且线程安全,相当于SqlSession的代理,可以替换SqlSession) -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

<!-- 配置Mapper接口包扫描 用于向容器中注册Mapper接口代理对象 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="换成你的mapper包" />
</bean>

<!-- Spring事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <constructor-arg ref="dataSource" />
</bean>

<!-- Spring声明式事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

有了上述配置,就可以愉快的使用 Spring 声明式事务了。

2. 源码解析

通过上一节的配置,可以了解到有哪些组件参与到 Spring 和 Mybatis 的整合:SqlSessionFactoryBeanSqlSessionTemplateMapperScannerConfigurer

其中 MapperScannerConfigurer 用于给 Mapper 接口注册代理 bean,和事务的关系不大,本文就不做过多说明了。主要讲解下 SqlSessionFactoryBean 和 SqlSessionTemplate。

2.1. SqlSessionFactoryBean——让 SqlSessionFactory 的初始化融入 Spring

前面提到 mybatis-spring 通过SqlSessionFactoryBean将 mybaits 启动过程整合进 Spring 的生命周期中。

相关代码如下:

public class SqlSessionFactoryBean 
        implements FactoryBean<SqlSessionFactory>, 
                InitializingBean, ApplicationListener<ApplicationEvent>{

    // .......省略部分代码

    @Override
    public void afterPropertiesSet() throws Exception {
      // ...略
      this.sqlSessionFactory = buildSqlSessionFactory();
    }
    
    @Override
    public SqlSessionFactory getObject() throws Exception {
      if (this.sqlSessionFactory == null) {
        afterPropertiesSet();
      }
      return this.sqlSessionFactory;
    }
}

了解 Spring FactoryBean 的同学应该知道,FactoryBean 在自动注入的时候,不会注入自身而是调用 FactoryBean#getObjectSqlSessionFactory 初始化相关流程发生在 bean 初始化阶段,详细代码在buildSqlSessionFactory()中,这里不再罗列代码了,其大致流程如下:

上述流程中,注册 Myabits 事务工厂 SpringManagedTransactionFactory 是 Mybatis 事务整合进 Spring 事务的关键一环,它用于创建由 Spring 管理的 SpringManagedTransaction(详情见 2.3);

2.2. SqlSessionTemplate ——让 SqlSession 实现线程安全的秘密

【问题背景】

原生 Mybatis 中, SqlSession 实现类 DefaultSqlSession 本身是线程不安全的,一个 SqlSession 实例只能被一个线程所持有,用完即丢。

但是到了 Spring 中,SqlSession 线程不安全会导致@Mapper代理类线程不安全:@Mapper的动态代理对象持有一个 SqlSession 实例,在应用启动时,@Mapper的动态代理对象就已经被注入到其他 bean 中了。

一般来说,我们所写的 Controller、Service 都需要是“无状态”的,即不存在线程安全问题。那 Mybatis 原生@Mapper线程不安全该如何解决呢?

Mybatis-Spring 的解决方案是动态代理

SqlSessionTemplate 实现了 SqlSession 接口,其内部持有一个 SqlSession 的代理对象 sqlSessionProxy,对 SqlSessionTemplate 实例的调用请求都会通过 sqlSessionProxy 路由给真正的 Mybatis SqlSession 实例(这个 SqlSession 实例是和当前线程绑定的)。这样就实现了 SqlSessionTemplate 的线程安全,所以 SqlSessionTemplate 实例可以在 Spring 容器中以单例的形式存在。

在创建 Mapper 代理时,将 SqlSessionTemplate 实例传给@Mapper代理对象即可保证 Mapper 也是线程安全的。

SqlSessionTemplate 实例化及代理逻辑如下:

public class SqlSessionTemplate implements SqlSession, DisposableBean {

  // ...
  private final SqlSession sqlSessionProxy;

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

  @Override
  public <T> T getMapper(Class<T> type) {
    // 创建Mapper代理时,传入SqlSessionTemplate实例
    return getConfiguration().getMapper(type, this);
  }

  // ...

  // JDK 动态代理拦截器
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 1. 获取SqlSession,优先获取本线程绑定的SqlSession 这里调用的工具类是SqlSessionUtils#getSqlSession
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 2. 调用真正的sqlSession逻辑
        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) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }
}

代理逻辑中,获取 sqlSession 实例的代码如下:

public final class SqlSessionUtils {

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    // 1. 优先获取当前线程绑定的sqlSession实例
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    // 2. 当前线程未绑定,开启新的sqlSesson实例
    session = sessionFactory.openSession(executorType);

    // 3. 如果当前线程开启了事务,将sqlSession绑定到当前线程
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }
}
    1. 通过 Spring 工具类 TransactionSynchronizationManager 获取当前线程绑定的sqlSession实例,非空则返回
    1. 若线程未绑定sqlSession,通过 sqlSessionFactory 获取新的 sqlSession 实例;
    1. 根据条件,选择性的将sqlSesson绑定到当前线程:
    • 绑定条件:当前线程开启了事务,并且, 事务工厂是 SpringManagedTransactionFactory 实例(默认情况就是,事务工厂的注册逻辑在 SqlSessionFactoryBean);

【SqlSessionTemplate 总结】

  1. 解决了 SqlSession 线程不安全问题;
  2. 支持 Spring 事务:在开启 Spring 事务的情况下,将调用请求 路由 到正确的 sqlSession 实例(当前线程所绑定的 sqlSession)

好了,到这里我们就分析完了 Mybatis 是如何融入 Spring 生命周期的,以及 Mybatis 中 SqlSession 线程不安全问题是如何解决的。接下来,将分析 Spring 事务和 Mybatis 事务是怎么整合在一起的。

2.3. SpringManagedTransaction——让 Spring 事务和 SqlSession 使用相同的 JDBC Connection

2.3.1. SqlSession 中的数据库连接从何而来的

先来了解下 Mybatis 中 SqlSession、Executor、Transaction 和 JDBC Connection 的关系:

简单来说,SqlSession、Executor、Transaction 和 JDBC Connection 就是“套娃”的关系,经过层层嵌套,SqlSession 实例间接持有一个 Transaction 实例,通过 Transaction 获取数据库连接。

那 SqlSession 实例是怎么被创建出来的呢?

来看看 SqlSessionFactory 创建 SqlSession 的逻辑:

public class DefaultSqlSessionFactory implements SqlSessionFactory {

  @Override
  public SqlSession openSession(ExecutorType execType) {
    return openSessionFromDataSource(execType, null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
      boolean autoCommit) {
    Transaction tx = null;
    try {
      // 1. 获取Environment,其内部封装了 DataSource 和 TransactionFactory
      final Environment environment = configuration.getEnvironment();
      //  前面提到的 SpringManagedTransactionFactory,在此处起作用
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 2. 实例化 Mybaits Transaction -> SpringManagedTransaction 实例
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 3. 实例化 Executor
      final Executor executor = configuration.newExecutor(tx, execType);
      // 4. 实例化 SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

}

2.3.2. 通过 SpringManagedTransaction 获取数据库连接

SpringManagedTransaction 获取数据库连接的代码如下:

public class SpringManagedTransaction implements Transaction {

  // ...
  @Override
  public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  private void openConnection() throws SQLException {
    // DataSourceUtils.getConnection会优先返回当前线程绑定的connection
    // 感兴趣的同学可以自行查看源码,基本原理就是通过ThreadLocal获取
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();

    // 保存当前连接是否为事务连接
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    LOGGER.debug(() -> "JDBC Connection [" + this.connection + "] will"
        + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
  }

  @Override
  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
      this.connection.commit();
    }
  }
  // ...
}

SpringManagedTransaction 实现了 Mybatis 的 Transaction 接口,负责管理 JDBC Connection 的生命周期,

它有两个主要功能:

  • 1、通过 dataSource,获取数据库连接;
    • 如果开启了事务DataSourceUtils.getConnection(this.dataSource)返回就是开启事务的 connection;
    • 否则,则从 datasource 中新获取一个 connection;
  • 2、 控制commit/ rollback/ close的逻辑(代码没贴全,下面以 commit操作为例)
    • 如果 Spring 事务处于开启状态 ,那么对于它的 commit/ rollback/ close 调用都将被忽略,因为 Spring 的事务管理器会自动完成事务提交、回滚操作;
    • 如果没有开启 Spring 事务,它就和 JdbcTransaction 是等价的;

【总结】

SqlSession 最终是通过 Transaction 接口获取数据库连接。

SpringManagedTransaction 实现了 Mybaits 的 Transaction 接口,在获取数据库连接时会优先获取当前线程绑定的数据库连接(原理是 ThreadLocal),Spring 开启事务后会将数据库连接绑定到当前线程,这样一来 SqlSession 实例拿到的是正确的数据库连接了。

3. Mybatis + Spring 声明式事务全流程示例

下面粗略展示了一个带有@Transactional注解的方法的执行过程(只标注了关键步骤),希望可以帮助你理解 Mybatis 整合进 Spring 的原理:

4. 总结

本文通过配置文件分析了 Spring 整合 Mybatis 的原理。SqlSessionTemplate 通过动态代理拦截 mapper 调用请求,将 Mybatis 的 SqlSession 实现类 DefaultSqlSession 和线程绑定,解决线程不安全问题;SpringManagedTransaction 将 Mybatis 事务行为托管给 Spring,最终通过 ThreadLocal 来保证 Spring 事务和 SqlSession 使用相同的 JDBC Connection。