上篇文章讲解了 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 将会自动创建SqlSessionFactory;SqlSessionFactoryBean通过 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 的整合:SqlSessionFactoryBean、SqlSessionTemplate 和 MapperScannerConfigurer。
其中 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#getObject;SqlSessionFactory 初始化相关流程发生在 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;
}
}
-
- 通过 Spring 工具类 TransactionSynchronizationManager 获取当前线程绑定的sqlSession实例,非空则返回
-
- 若线程未绑定sqlSession,通过 sqlSessionFactory 获取新的 sqlSession 实例;
-
- 根据条件,选择性的将sqlSesson绑定到当前线程:
- 绑定条件:当前线程开启了事务,并且, 事务工厂是 SpringManagedTransactionFactory 实例(默认情况就是,事务工厂的注册逻辑在 SqlSessionFactoryBean);
【SqlSessionTemplate 总结】
- 解决了 SqlSession 线程不安全问题;
- 支持 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 是等价的;
- 如果 Spring 事务处于开启状态 ,那么对于它的
【总结】
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。