摘要:本文结合mybatis-spring模块源码,详细讲解了spring如何集成mybatis框架,如何解决对象管理、线程安全、统一事务等问题。
Spring 集成Mybatis,需要解决以下两个核心问题:
- 接管 MyBatis 核心对象,将SqlSession、SqlSessionFactory、Mapper等对象交由Spring管理;
- 统一 两个框架事务机制
现在我们一起来看看mybatis-spring模块如何解决上述问题。
注:本文中源码来自mybatis 3.4.x、mybatis-spring 2.1.x
一 使SqlSession线程安全
1.1 非线程安全
MyBatis 中 SqlSession 本身是非线程安全的, 如果多个线程共享一个 SqlSession,会导致事务混乱、数据脏读、SQL 执行异常等问题。

为什么呢?不安全的原因有以下几点:
- 对状态共享的操作,都是非原子的
java
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit; // 共享状态
private boolean dirty; // 共享状态
private List<Cursor<?>> cursorList; // 共享状态
}
- Executor对象中,一级缓存、事务都是非线程安全的
java
protected Transaction transaction;
// 一级缓存
protected PerpetualCache localCache;
1.2 线程安全方案
使用SqlSessionManager,通过ThreadLocal将SqlSession与线程绑定。
java
// SqlSessionManager通过ThreadLocal实现线程安全
SqlSessionManager manager = SqlSessionManager.newInstance(sqlSessionFactory);
manager.startManagedSession(); // 绑定到当前线程
// 多线程安全使用manager
Spring集成Mybatis,提供了SqlSessionTemplate
java
// Spring通过ThreadLocal管理SqlSession
@Autowired
private SqlSessionTemplate sqlSessionTemplate; // 线程安全
1.3 SqlSessionTemplate
Spring 集成 MyBatis 时,通过 ThreadLocal 绑定 SqlSession 到当前线程 + SqlSessionTemplate 封装 ,让每个线程拥有独立的 SqlSession 实例,从根本上避免多线程共享带来的线程安全问题。

SqlSessionTemplate持有SqlSession的代理对象,增加了从当前线程中获取SqlSession的逻辑。
java
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
// 代理
private final SqlSession sqlSessionProxy;
// 从当前线程中获取SqlSession
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
// 通过 TransactionSynchronizationManager(ThreadLocal实现)获取当前线程的SqlSessionHolder
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
// 线程中有则复用
return session;
}
session = sessionFactory.openSession(executorType);
// 绑定SqlSession到当前线程
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
// 简化代码
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
return method.invoke(sqlSession, args);
}
}
SqlSessionTemplate实现了SqlSession接口,方法调用转交给sqlSessionProxy。
二 创建Mapper代理对象
1.1 Mybatis中Mapper多例
mybatis中MapperRegistry#getMapper方法,每次都会返回新的代理对象。

原因在于入参SqlSession 是线程不安全的,Mapper 对象需绑定当前的 SqlSession。
java
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
// 获取 Mapper 代理对象
UserMapper mapper1 = session1.getMapper(UserMapper.class);
UserMapper mapper2 = session1.getMapper(UserMapper.class);
UserMapper mapper3 = session2.getMapper(UserMapper.class);
// 结果:
// mapper1 != mapper2 (不同的代理对象)
// mapper1 != mapper3 (不同的代理对象)
// mapper2 != mapper3 (不同的代理对象)
1.2 Spring中注册Mapper单例
在mybatis-spring.jar中,提供了@MapperScan注解,从而引入@Import(MapperScannerRegistrar.class) 。

MapperScannerRegistrar实现了ImportBeanDefinitionRegistrar接口,会将Mapper接口信息处理为BeanDefinition对象,同时设置BeanDefinition的beanClass属性为MapperFactoryBean.class。
很显然,MapperFactoryBean实现了FactoryBean接口,getObject方法会创建Mapper代理对象(单例的),此对象持有的SqlSession其实是sqlSessionTemplate实例。
// MapperFactoryBean中,Mapper对象会缓存到Spring单例池中
public boolean isSingleton() {
return true;
}


总结一下,就是:
- @MapperScan指定Mapper接口包路径,由spring扫描,将Mapper接口注册为MapperFactoryBean类型;
- spring会创建SqlSessionTemplate的单例bean,并注入到MapperFactoryBean对象中;
- spring会创建Mapper接口的代理对象,并缓存到单例池中,而该对象持有的SqlSession其实是sqlSessionTemplate对象。
- @Resource注入Mapper对象时,将从单例池中获取;
- Mapper对象调用如selectBy*等方法时,将使用当前线程绑定的sqlSession(没有则创建),来执行SQL语句。
因此,spring中Mapper代理对象是单例且线程安全。
三 统一事务管理
mybatis中提供了Transaction接口,是对事务管理的核心抽象,负责封装数据库连接并管理其生命周期。提供了如下方法:
- getConnection() - 获取数据库连接
- commit() - 提交事务
- rollback() - 回滚事务
该接口有两个实现
-
JdbcTransaction,直接使用JDBC的事务管理机制,

-
ManagedTransaction,将事务管理委托给外部框架,如Spring;因此对commit、rollback方法做了空实现

spring整合Mybatis的事务管理,关键问题就是如何保证执行SQL和事务管理器提交或回滚事务时使用同一个Connection对象。
mybatis-spring中提供了SpringManagedTransaction,该类的openConnection()会从threadLocal中获取连接,没有的话则由spring负责创建一个,并绑定到当前线程。
java
public class SpringManagedTransaction implements Transaction {
private final DataSource dataSource;
private Connection connection;
// 是否是事务连接,是则将commit/rollback调用委托给Spring事务管理器
private boolean isConnectionTransactional;
private boolean autoCommit;
}

SpringManagedTransaction的commit/rollback方法,判断当前连接如果带事务,将会委托给Spring事务管理器来处理。

Spring提交或回滚事务时,也将从当前线程的ThreadLocal中获取连接,这样就保证了与Mybatis执行SQL使用同一个连接。