Spring集成Mybatis原理详解

摘要:本文结合mybatis-spring模块源码,详细讲解了spring如何集成mybatis框架,如何解决对象管理、线程安全、统一事务等问题。

Spring 集成Mybatis,需要解决以下两个核心问题:

  1. 接管 MyBatis 核心对象,将SqlSession、SqlSessionFactory、Mapper等对象交由Spring管理;
  2. 统一 两个框架事务机制

现在我们一起来看看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;
  }

总结一下,就是:

  1. @MapperScan指定Mapper接口包路径,由spring扫描,将Mapper接口注册为MapperFactoryBean类型;
  2. spring会创建SqlSessionTemplate的单例bean,并注入到MapperFactoryBean对象中;
  3. spring会创建Mapper接口的代理对象,并缓存到单例池中,而该对象持有的SqlSession其实是sqlSessionTemplate对象。
  4. @Resource注入Mapper对象时,将从单例池中获取;
  5. 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使用同一个连接。

相关推荐
想用offer打牌12 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
曹牧13 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
KYGALYX13 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法14 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
kfyty72514 小时前
集成 spring-ai 2.x 实践中遇到的一些问题及解决方案
java·人工智能·spring-ai
猫头虎14 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven
李少兄14 小时前
在 IntelliJ IDEA 中修改 Git 远程仓库地址
java·git·intellij-idea
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
忆~遂愿14 小时前
ops-cv 算子库深度解析:面向视觉任务的硬件优化与数据布局(NCHW/NHWC)策略
java·大数据·linux·人工智能