SSM 整合 - Spring 与 MyBatis 的整合细节

先列出 Spring 与 Mybatis 整合后,Spring 的配置文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>

<!--引入外部的属性配置文件-->
<context:property-placeholder location="jdbc.properties"/>

<!--数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<!--注册 SqlSessionFactoryBean:通过该 Bean 获取 SqlSessionFactory 核心对象,从而再获取 SqlSession 对象-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!--配置 Mybatis 核心配置文件路径-->
<property name="configLocation" value="mybatis-config.xml"/>
<!--配置数据源-->
<property name="dataSource" ref="dataSource"/>
<!--配置包别名-->
<property name="typeAliasesPackage" value="com.powernode.bank.pojo"/>
</bean>

<!--注册 Mapper 扫描配置器:指定扫描的包路径,为包下所有的 mapper 接口,动态生成接口代理类-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.powernode.bank.mapper"/>
</bean>

<!--事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!--开启事务注解驱动器-->
<tx:annotation-driven transaction-manager="txManager"/>

</beans>

细节一:SqlSessionFactoryBean 是如何完成配置转移的

Spring 与 MyBatis 集成后,MyBatis 核心配置文件中绝大多数的配置,都可以很方便地转移到 Spring 的配置文件中来。具体是怎么完成转移的呢?关键就在于,新加入的依赖 mybatis-spring 中提供了一个功能强大的 SqlSessionFactoryBean

回忆 Mybatis 的内容,我们提供 Mybatis 的配置文件,其实就是要根据该配置文件的内容,去创建 SqlSessionFactory 对象,然后再从中获取 SqlSession 对象来操作数据库:

1
2
3
InputStream is = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
sqlSession = sqlSessionFactory.openSession();

转换到 Spring 的语境下,其实就是要在 IoC 容器中注册一个 SqlSessionFactory 对象。因此 mybatis-spring 就直接提供了一个工厂 Bean 对象 SqlSessionFactoryBean,用于辅助 Spring 创建 SqlSessionFactory 对象。

SqlSessionFactoryBean 提供了非常多的属性来帮助我们转移 MyBatis 核心配置文件中的配置。

比如 MyBatis 配置文件中的 <environment> 标签下的 <dataSource> 标签配置,就对应了 SqlSessionFactoryBeandataSource 属性:在 Spring 配置文件下,我们可以先声明一个数据源如 DruidDataSource,然后注入到 SqlSessionFactoryBeandataSource 属性中。

细节二:<transactionManager type="JDBC"/> 的底层原理

<transactionManager> 用于配置 Mybatis 的事务管理器,属性 type 有两个可选值:JDBCMANAGED

  • 如果 typeJDBC 的话,那么所构建的 SqlSessionFacory 对象中所采用的 TransactionFactory 就是 JdbcTransactionFactory,创建出来的 SqlSession 底层采用的就是 JdbcTransaction 来管理事务。

  • 如果 typeMANAGED 的话,那么所构建的 SqlSessionFacory 对象中所采用的 TransactionFactory 就是 ManagedTransactionFactory,创建出来的 SqlSession 底层采用的就是 ManagedTransaction 来管理事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// SqlSessionFactoryBuilder
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
return build(parser.parse());
}

// XMLConfigBuilder
public Configuration parse() {
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

// XMLConfigBuilder
private void parseConfiguration(XNode root) {
// ...
environmentsElement(root.evalNode("environments"));
// ...
}

// XMLConfigBuilder
private void environmentsElement(XNode context) throws Exception {
// ...
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// ...
}

// XMLConfigBuilder
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
String type = context.getStringAttribute("type");
TransactionFactory factory = (TransactionFactory) resolveClass(type).getDeclaredConstructor().newInstance();
return factory;
}

从源码可以看到,Mybatis 的事务管理器,其实指的是 SqlSessionFactory 所采用的事务工厂 TransactionFactory

细节三:MapperScannerConfigurer 的底层原理

回顾之前学习 Mybatis 的时候,我们是使用 sqlSession.getMapper(AccountMapper.class) 来获取接口的动态代理的。

而在 Spring + Mybatis 的整合场景下,我们没有显示地去编写 sqlSession.getMapper 的代码,而是通过在 Spring 配置文件中配置了一个 MapperScannerConfigurer 接口扫描配置器,让该配置器为我们生成接口代理并注册到 Spring IoC 容器中去。

1
2
3
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.powernode.bank.mapper"/>
</bean>

下面将深入源码,说明 MapperScannerConfigurer 的底层的执行过程。

了解 BeanDefinitionRegistryPostProcessor 接口原理

我们先来关注 MapperScannerConfigurer 所实现的 BeanDefinitionRegistryPostProcessor 接口。

BeanDefinitionRegistryPostProcessor 接口的源码如下:

1
2
3
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}

接口中的 postProcessBeanDefinitionRegistry 方法是 Spring 框架所提供的一个扩展点:在所有的 BeanDefinition 注册到 Spring 以后,在 Spring 根据这些 BeanDefinition 来进行实例化以前,Spring 会调用这个方法,并传入保存了当前所有 BeanDefinitionBeanDefinitionRegistry

所以,如果你希望在所有的 BeanDefinition 完成注册之后,先对这些 BeanDefinition 做一些修改和调整,然后再让 Spring 去根据这些 BeanDefinition 去实例化 Bean,那么你就可以定义一个类,让这个类实现 BeanDefinitionRegistryPostProcessor 接口中的 postProcessBeanDefinitionRegistry 方法,在方法中进行 BeanDefinition 的修改和调整,最后再将该类声明到 Spring 配置文件中。

BeanDefinition 其实就是 Bean 定义信息,可以将其理解为 Spring 配置文件中的 Bean 标签对应的配置信息封装类,其中包含了 Bean 的对应的 Class 对象,以及要注入的属性等信息。

如果在 postProcessBeanDefinitionRegistry 方法中修改了 BeanDefinition 关联的 Class 对象,比如从 A.class 修改为 B.class,那么 Spring 根据修改后的 BeanDefinition 实例化得到的是 B 对象而非 A 对象。这一点是我们后面将 Mapper 接口改造为 MapperFactoryBean 的关键。

BeanDefinitionRegistry 其实就是 Bean 定义信息注册中心,Spring 容器在初始化时,会读取配置文件中所有的 Bean 标签,对于每个 Bean 标签,封装为对应的一个 BeanDefinition 然后注册到 BeanDefinitionRegistry 中。

扫描阶段:MapperScannerConfigurer 扫描指定包下的所有 Mapper 接口,为其创建 BeanDefinition 并注册到 Spring 中

MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,并在声明到了 Spring 配置文件中。

所以当所有的 BeanDefinition 注册到 Spring 容器的 BeanDefinitionRegistry 以后,会调用 MapperScannerConfigurer 所实现的接口方法 postProcessBeanDefinitionRegistry

1
2
3
4
5
6
7
8
9
10
// MapperScannerConfigurer
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 创建类路径 Mapper 扫描器,并初始化扫描器的一些参数
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// 其中一个比较重要的扫描器参数的初始化
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
// 扫描指定的 basePackage 包,对读到的每个 Mapper 接口做一些处理
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}

我们先来看方法的参数,Spring 调用该方法时传入的 registry 参数保存了当前所有已注册的 BeanDefinition,此时打断点可以看到,目前 registry 中还不包含 Mapper 接口相关的 BeanDefiniton

方法内部首先创建了一个 Mapper 扫描器,并使用 MapperScannerConfigurer 类内部的成员变量,对该扫描器的一些参数进行初始化。

在 Spring 配置文件中声明 MapperScannerConfigurer 时,我们并没有为其 mapperFactoryBeanClass 属性提供值,该属性也没有默认初始化,所以 this.mapperFactoryBeanClassnull,此时 scanner 就会使用一个默认的 Class 对象 MapperFactoryBean.class 来对内部的 mapperFactoryBeanClass 属性进行赋值,源码如下:

1
2
3
public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) {
this.mapperFactoryBeanClass = mapperFactoryBeanClass == null ? MapperFactoryBean.class : mapperFactoryBeanClass;
}

扫描器创建并初始化配置完成后,就开始进行扫描了。Mapper 接口的扫描逻辑在 scanner.scan 方法中,打开该方法,发现主要的逻辑,有都放在了 doScan 方法内。

1
2
3
4
5
6
7
8
public int scan(String... basePackages) {
// 获取 Mapper 扫描前,已经注册的 BeanDefinition 的数量
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
// 完成包扫描,其中会注册一些与 Mapper 接口相关的新的 BeanDefinition
doScan(basePackages);
// 返回一个新注册的 BeanDefinition 的数量
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

再来看 doScan 方法,该方法主要分为两步:

  • 第一步:先扫描指定 basePackage 及其子包下的所有 Mapper 接口,为每个 Mapper 接口创建一个 BeanDefinition 对象,并注册到 Spring 中。
    假设 basePackage 下有一个 AccountMapper 接口,那么 super.doScan() 执行完以后,Spring 的 BeanDefinitionRegistry 中包含一个新的 BeanDefinition,其名字为 accountMapper,其对应的 Class 对象为 AccountMapper.class

  • 第二步:每个 Mapper 接口本身作为接口,是无法实例化的,因此需要经过 processBeanDefinitions() 方法对每个 Mapper 接口对应的 BeanDefinition 进行调整。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 扫描指定包下的所有 Mapper 接口,并创建相应 BeanDefinition 注册到 Spring 中
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

// 如果有 Mapper 接口创建了相应的 BeanDefinition,就对这些 BeanDefinition 进行调整。
if (!beanDefinitions.isEmpty()) {
processBeanDefinitions(beanDefinitions);
}

return beanDefinitions;
}

调整/修改阶段:MapperScannerConfigurer 对新创建 BeanDefinition 进行调整,确保 Spring 根据调整后的 BeanDefinition 所实例化的是一个 MapperFactoryBean 对象

processBeanDefinitions 方法内部具体是怎么调整的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
BeanDefinitionRegistry registry = getRegistry();
// 遍历每个 Mapper 接口对应的 BeanDefinition,对其进行调整
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
// 调整一:添加一个初始化实参值,值为相应 Mapper 接口的全类名
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
// 调整二:修改 `BeanDefinition` 关联的 Class 对象为 MapperFactoryBean.class
definition.setBeanClass(this.mapperFactoryBeanClass);
// 调整三:将 `BeanDefinition` 的装配模式设置为按类型自动装配
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}

假设我们正在调整的是 AccountMapper 对应的 BeanDefinition

  • 我们先来看调整二,这一步是最关键的一步。

调整前,BeanDefinition 中所关联的 Class 对象是接口 Class 对象 AccountMapper.class,接口 Class 显然是无法进行实例化的,所以修改为 MapperFactoryBean.class。这样后续 Spring 在根据调整后的 BeanDefinition 进行实例化时,创建的就是 MapperFactoryBean 对象了。

MapperFactoryBean 是一个实现了 FactoryBean 接口的工厂 Bean 对象,其内部有一个非常重要的 mapperInterface 属性,该属性绑定了某个 Mapper 接口的 Class 对象比如 AccountMapper.class,后续将会以构造注入的方式赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {

private Class<T> mapperInterface;

public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}

@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}

可以看到,MapperFactoryBeangetObject 方法就是为内部绑定的 Mapper 接口生成相应的接口代理。

  • 我们再来看调整一,这一步主要是提供 MapperFactoryBeanmapperInterface 属性的初始化参数,方便对其进行构造注入。

这里面有个小细节要说明一下,MapperFactoryBean 中的 mapperInterface 是 Class 类型,但是我们在调整一中所设置的初始化参数,却是有个全类名字符串,比如 com.powernode.bank.mapper.AccountMapper,这能成功注入吗?

答案是可以的,因为 Class 类型是 Spring 中的简单类型,所以直接提供全类名字符串作为 value,Spring 底层会为我们做相应的类型转换,得到全类名对应的类的 Class 对象再进行注入!

  • 我们最后来看调整三,这一步确保 MapperFactoryBean 继承自父类的 sqlSessionTemplate 属性,后续能够通过按类型的自动装配附上值。

还记得在调整一中 getObject() 方法内部调用的 getSqlSession() 吗?其实这个是父类 SqlSessionDaoSupport 的方法。我们来看 SqlSessionDaoSupport 中比较关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class SqlSessionDaoSupport extends DaoSupport {

private SqlSessionTemplate sqlSessionTemplate;

public SqlSession getSqlSession() {
return this.sqlSessionTemplate;
}

public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
}
}

protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}

因为 MapperFactoryBean 继承了 SqlSessionDaoSupport,所以 MapperFactoryBean 类中也包含了继承自父类的 setSqlSessionFactory 方法。

在调整三处,我们将 BeanDefinition 的装配模式设置为按类型自动装配,这样后续 Spring 在实例化时,就会去查找 MapperFactoryBean 类中的所有 setter 方法并进行调用。

此时 setSqlSessionFactory 方法就得到调用,该方法根据传入的 sqlSessionFactory,创建一个新的 SqlSessionTemplate 赋值给内部的 sqlSessionTemplate 属性,从而 getObject() 方法内部调用的 getSqlSession() 就能获取到值了!

MapperFactoryBean 到 Mapper 接口的代理类

当 Service 类需要注入相应的 Mapper 接口时,就会从三级缓存中找到该 Mapper 接口对应的 MapperFactoryBean,然后调用 getObject() 方法生成最终的代理类,并注入到 Service 中。

之前我们说过 MapperFactoryBeangetObject() 方法内部,依然是调用 sqlSession.getMapper 来生成接口代理的:

1
2
3
4
// MapperFactoryBean
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}

只不过,这里 getSqlSession() 获取到的是 SqlSessionTemplate 对象,而不是之前学习 Mybatis 时的 DefaultSqlSession 对象。

SqlSessionTemplate 是一个有趣的对象,它在整个应用中是单例存在,由于它也实现了 SqlSession 接口,所以可以伪装成 SqlSession 对象来调用 getMapper 方法为所有 Mybatis Mapper 接口生成代理类。

最后补充说明一点,虽然每个生成并注册到 Spring 中的接口代理类,其内部持有的 sqlSession 属性都指向 SqlSessionTemplate 单例,但是 SqlSessionTemplate 本身并没有在 Spring 容器中进行注册。

细节四:Mybatis 的事务管理器和 Spring 的事务管理器 txManager 的关系是什么?

Spring 的事务管理器是在 spring-jdbc 依赖中提供的,在不集成 Mybatis 的情况下,或者说世界上没有 Mybatis 框架的情况下,Spring 也有自己的事务管理器实现。

因此,Mybatis 的事务管理器,和 Spring 的事务管理器,其实并不是同一个东西:

  • Mybatis 的事务管理器,本质上是一个是面向 SqlSession 的事务工厂。
    每个 SqlSession 创建时,事务工厂会为其创建一个 Transaction 对象并放到 SqlSession 内部。调用 sqlSession.commit() 等事务方法时,底层是调用内部 Transaction 对象的事务方法,对 Transaction 对象中的 Connection 连接进行操作。

  • Spring 的事务管理器,是直接面向 Connection 连接对象的。

Mybatis 和 Spring 的事务管理器底层一定都是对 Connection 对象进行事务操作。只不过 Mybatis 的事务管理器相对而言就不那么纯粹,它还附带了一些 Connection 对象与 SqlSession 对象整合的逻辑。

Mybatis 和 Spring 的事务管理器不是同一个东西,那在 Spring + Mybatis 的整合场景下(回顾最开始的 Spring 配置文件),这两个事务管理器分别是什么?

  • Mybatis 的事务管理器是 SpringManagedTransactionFactory 事务工厂。

SqlSessionFactoryBean 的内部源码可以看出这一点:

1
2
3
4
5
6
7
8
9
10
11
// SqlSessionFactoryBean
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
// ...
Environment env = new Environment(
this.environment,
this.transactionFactory == null ? new SpringManagedTransactionFactory()
: this.transactionFactory,
this.dataSource);
targetConfiguration.setEnvironment(env);
// ...
}

在 Spring 配置文件中,如果没有明确为 SqlSessionFactoryBean 注入 transactionFactory 属性,那么就会默认创建 SpringManagedTransactionFactory 来作为事务工厂。

一般来说,在 Spring + Mybatis 的整合场景下,我们不会为 SqlSessionFactoryBean 注入 transactionFactory 属性。Mybatis 中文网关于 <transactionManager> 也有相应说明:

如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

  • Spring 的事务管理器是 txManager,也即 DataSourceTransactionManager

细节五:在 Spring 声明式事务下,Spring 和 Mybatis 的两个事务管理器是如何协同工作的?

Mybatis 和 Spring 的事务管理器不是同一个东西,但它们底层又都是对 Connection 对象进行操作,那在 Spring 声明式事务下,当一个被 @Transactional 标注的业务方法被调用时,这两个事务管理器是怎么协同工作的?

第一阶段:Spring AOP 拦截业务方法的调用,为当前线程创建并开启事务

当一个被 @Transactional 标注的业务方法被调用时,Spring AOP 首先会进行拦截,在其动态代理的增强中,使用已注册的 txManager 来进行事务控制:

  • 首先会尝试获取当前线程的事务,发现获取不到,于是就会去开启一个事务
1
2
3
4
5
6
7
8
9
// AbstractPlatformTransactionManager
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
Object transaction = doGetTransaction();
if (isExistingTransaction(transaction)) {
return handleExistingTransaction(def, transaction, debugEnabled);
} else {
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}
}
  • 开启事务的主要工作是:先从数据源中获取了一个 Connection 对象,关闭自动提交,然后将该连接放到一个 ThreadLocal 中。

更具体地说,从数据源中获取的 Connection 对象会先被封装到一个 ConnectionHolder 对象中,然后关闭连接的自动提交,最后将 ConnectionHolder 放到 TransactionSynchronizationManager 类中的 ThreadLocal<Map<Object, Object>> resources 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// DataSourceTransactionManager
protected void doBegin(Object transaction, TransactionDefinition definition) {
// 事务对象
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
// 如果事务对象没有 ConnectionHolder
if (!txObject.hasConnectionHolder()) {
// 从数据源中获取 Connection,包装为 ConnectionHolder 后放入事务对象中
Connection newCon = obtainDataSource().getConnection();
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
// 关闭事务对象的连接的自动提交
con = txObject.getConnectionHolder().getConnection();
con.setAutoCommit(false);
// 将 ConnectionHolder 绑定到 ThreadLocal 中
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}

注意,resources 为线程绑定的是一个 Map<Object, Object> 集合。当前线程第一次进行绑定时,resources.get() 为空,因此会创建一个空的 Map 集合绑定进去。

然后将数据源作为 key,将 ConnectionHolder 作为 value,放入该 Map 集合,从而完成 ConnectionThreadLocal 的绑定。

1
2
3
4
5
6
7
8
9
// TransactionSynchronizationManager
public static void bindResource(Object key, Object value) throws IllegalStateException {
Map<Object, Object> map = resources.get();
if (map == null) {
map = new HashMap<>();
resources.set(map);
}
map.put(key, value);
}

我们已经通过 Spring AOP 为当前线程创建并绑定了事务,接下来就要执行目标业务方法了,目标业务方法中就涉及到对 Mybatis 接口代理类的方法调用,如 accountMapper.selectByActno(actno)

第二阶段:Mybatis 接口代理类将 Mapper 接口方法的调用,转换为 SqlSession 数据库操作的调用。

Mybatis 接口代理类的主要工作是:在增强代码中,根据接口的方法信息和 Mapper 映射配置文件信息,确定最终要执行的 SqlSession 的数据库操作并执行。

比如 accountMapper.selectByActno(actno) 经过增强代码的分析后,能够最终去调用 sqlSession.selectList() 来执行数据库操作。

需要注意的是,这里动态代理转换后,最终确定的 sqlSession.selectList() 中的 sqlSession 是一个 SqlSessionTemplate 类型的对象,而不是 DefaultSqlSession 类型的对象。

第三阶段:SqlSessionTemplate 通过动态代理将要执行的数据库操作转发给绑定到当前线程的 DefaultSqlSession 来执行

现在终于要执行数据库操作了,比如 sqlSession.selectList(),此时 sqlSession 是一个 SqlSessionTemplate 类型的对象。

SqlSessionTemplate 是 Spring + Mybatis 整合场景下,解决 SqlSession 线程安全问题的关键对象。该对象虽然实现了 SqlSession 接口,但所有的数据库操作,实际上都是其内部封装的属性 sqlSessionProxy 来执行的。

1
private final SqlSession sqlSessionProxy;

sqlSessionProxy 本身也不实际执行数据库操作,正如它的名字,它是一个 SqlSession 接口的动态代理。实际的数据库操作,在 sqlSessionProxy 所绑定的调用处理器中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}

private class SqlSessionInterceptor implements InvocationHandler {
// 代理增强:获取当前线程的 SqlSession 对象,类型为 DefaultSqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
// 目标方法:通过当前线程的 DefaultSqlSession 对象来执行数据库操作
Object result = method.invoke(sqlSession, args);
return result;
}

所以,问题的关键就是 getSqlSession 底层到底是怎么获取当前线程的 SqlSession 对象的。

先说明一下 getSqlSession 的第一个参数,SqlSessionTemplate 这个类中还包括一个 SqlSessionFactory 属性,该属性注入的就是 SqlSessionFactoryBeangetObject() 的结果,是一个 DefaultSqlSessionFactory 类型的对象。

SqlSessionTemplate 内置该属性的目的就是,当获取不到当前线程的 SqlSession 对象时,通过该属性可以去创建一个!所以,在上面的注释中,我说获取到当前线程的 SqlSession 对象是 DefaultSqlSession 类型的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
// 获取当前线程的 SqlSessionHolder 对象
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 取出当前线程 SqlSession
SqlSession session = holder.getSqlSession();
// 如果当前线程的 SqlSession 存在,直接返回
if (session != null) {
return session;
}
// 否则不存在,利用 sessionFactory 创建新的 SqlSession
session = sessionFactory.openSession(executorType);
// 创建完成后,将该新建的 SqlSession 封装为 SqlSessionHolder 绑定到当前线程上
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}

在第一阶段中,我们就是将当前线程的 Connection 封装为一个 ConnectionHolder 绑定到 resources 中。但是这里要获取的不是 ConnectionHolder 而是 SqlSessionHolder,所以第一次执行时,这里 resources.get() 所得到的 Map<Object, Object> 集合中只包括 (dataSource, ConnectionHolder) 这一个键值对。

既然获取不到,我们就 sessionFactory.openSession() 创建一个新的 SqlSession 来返回。

在返回之前,我们还需要做一些工作:将新的 SqlSession 绑定到当前线程。具体看 registerSessionHolder 函数:

1
2
3
4
5
6
7
8
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
// 将 SqlSession 封装到 SqlSessionHolder 中
SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
// 以 sessionFactory 为 key,将 SqlSessionHolder 绑定到当前线程
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
// 这一步非常关键:注册事务同步!我们后面再说!
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
}

还记得我们在第一阶段,通过 Spring AOP 为当前线程绑定的 Connection 对象吧。

现在我们为当前线程绑定了两个对象:SqlSessionConnection。但显然,这两个对象之间还没建立起关系,所谓的建立起关系,就是将 SqlSession 底层的 Connection 设置为当前线程绑定的 Connection

关系的建立被推迟到数据库操作的执行时。回到 SqlSessionInterceptor 的目标方法部分,目前我们已经获取到了一个 DefaultSqlSession 类型的对象,该对象底层使用的事务管理器是 SpringManagedTransactionFactory

由于还没有第一次执行数据库操作,当前的 DefaultSqlSession 底层的 SpringManagedTransactionFactory 中的 Connection 还是 null

当真正执行时,会发现该 Connectionnull,然后从 resources 获取之前绑定到当前线程的 ConnectionHolder,从中取出 Connection 进行赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SpringManagedTransactionFactory
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}

// SpringManagedTransactionFactory
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
}

// DataSourceUtils
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
return doGetConnection(dataSource);
}

// DataSourceUtils
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
return conHolder.getConnection();
}

Mybatis 的事务管理器和 Spring 的事务管理器,底层所操作的 Connection 对象到这一步才终于达成了一致。

第四阶段:Spring AOP 在目标业务方法执行结束后,在后置增强中完成事务的提交、回滚与关闭

在 Spring 的事务管理中,业务方法作为 Spring AOP 的目标方法:

  • 其前置增强,是 Spring 事务管理器创建并开启事务
  • 其后置增强,是 Spring 事务管理器完成事务的提交、回滚与关闭

事务的提交、回滚与关闭,除了对底层的 Connection 对象执行 commit rollbackclose 以外,我们可能还需要做其他的一些操作,比如最容易想到的,在事务提交或回滚之前,从 resources 移除掉绑定到当前线程的 Connection 对象和 SqlSession 对象。

这些额外操作被称为事务同步操作,它们与事务本身的提交或回滚并不直接相关,它们可以在事务的不同阶段(事务的生命周期)被触发。

我们现在回过头来,应该就能明白第三阶段提到的,注册事务同步是什么意思了。

1
2
3
4
5
6
7
8
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
// 将 SqlSession 封装到 SqlSessionHolder 中
SqlSessionHolder holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
// 以 sessionFactory 为 key,将 SqlSessionHolder 绑定到当前线程
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
// 这一步非常关键:注册事务同步!我们后面再说!
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
}

Spring 在将 SqlSession 绑定到当前线程之后,紧接着立刻为其创建了一个 SqlSessionSynchronization 事务同步器,并将该事务同步器也绑定到当前线程。只不过 SqlSession 是绑定到 resources 上,事务同步器是绑定到 TransactionSynchronizationManager 类中的另外一个 ThreadLocal 变量 synchronizations 上。

1
2
3
4
// 为当前线程绑定 Connection 与 SqlSession
ThreadLocal<Map<Object, Object>> resources;
// 为当前线程的 SqlSession 绑定对应的事务同步器
ThreadLocal<Set<TransactionSynchronization>> synchronizations;

PS:SqlSessionSynchronizationmybatis-spring 提供的事务同步器实现,其中的事务同步操作已经写好了。

在目标业务方法执行完成以后,Spring AOP 实现会在其后置增强部分完成两部分工作:

  • 获取绑定到当前线程的 Connection,执行 commit rollback 以及 close 操作
  • 获取绑定到当前线程的事务同步器,在 commit 方法执行前后触发一些事务同步操作,在 rollback 方法执行前后触发一些事务同步操作等

Spring 的事务管理器中会对 Connectioncommit 操作,Mybatis 的 SqlSession 其底层的事务也会做 commit 操作。那会不会出现重复 commit 的情况呢?

答案是不会,可以看一下 Mybatis 的事务管理器 SpringManagedTransaction 的源代码:

1
2
3
4
5
6
7
@Override
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
LOGGER.debug(() -> "Committing JDBC Connection [" + this.connection + "]");
this.connection.commit();
}
}

注意 if 语句的第二个条件 !this.isConnectionTransactional,它表示检查当前连接对象是否被 Spring 事务所管理,如果是的话,将不会对 SqlSession 底层的 Connection 执行 commit 操作。

也就是说,Spring 的事务管理器会调用 Mybatis 的事务管理器去执行 commit 操作,只不过 Mybatis 事务管理器自己内部会拒绝提交,最终交给 Spring 的事务管理器来完成提交。

参考博客