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

相关推荐
Elieal2 小时前
SpringBoot 中处理接口传参时常用的注解
java·spring boot·后端
摇滚侠2 小时前
在 IDEA 中,GIT 合并分支时选择远程的 dev 分支和本地的 dev 分支,有区别吗
java·git·intellij-idea
cooldream20092 小时前
辩核AI具身辩论数字人训练系统:技术架构与功能体系全解析
人工智能·架构·具身数字人
Hello.Reader2 小时前
Flink Working Directory(FLIP-198)稳定本地恢复、RocksDB 目录与进程重启“不丢缓存”的正确姿势
spring·缓存·flink
fanruitian2 小时前
k8s 更新镜像
java·服务器·kubernetes
散峰而望2 小时前
【数据结构】假如数据排排坐:顺序表的秩序世界
java·c语言·开发语言·数据结构·c++·算法·github
En^_^Joy2 小时前
Kubernetes Pod控制器深度解析(K8s)
java·容器·kubernetes
LYOBOYI1232 小时前
qml程序运行逻辑
java·服务器·数据库
可问春风_ren2 小时前
Git命令大全
前端·javascript·git·后端