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使用同一个连接。

相关推荐
来杯@Java14 分钟前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
卷毛的技术笔记1 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥1 小时前
匿名函数 lambda + 高阶函数
java·python·算法
_codemonster1 小时前
30分钟快速搭建 Spring Cloud Alibaba 微服务实战(一)
微服务·架构·毕业设计·课程设计
会编程的土豆1 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木1 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar1 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
adrninistrat0r1 小时前
Java调用链MCP分析工具
java·python·ai编程
喵个咪2 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
噜噜噜阿鲁~2 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言