SSM 整合 - Spring 与 MyBatis 的整合细节
SSM 整合 - Spring 与 MyBatis 的整合细节
先列出 Spring 与 Mybatis 整合后,Spring 的配置文件内容:
1 |
|
细节一:SqlSessionFactoryBean
是如何完成配置转移的
Spring 与 MyBatis 集成后,MyBatis 核心配置文件中绝大多数的配置,都可以很方便地转移到 Spring 的配置文件中来。具体是怎么完成转移的呢?关键就在于,新加入的依赖 mybatis-spring
中提供了一个功能强大的 SqlSessionFactoryBean
。
回忆 Mybatis 的内容,我们提供 Mybatis 的配置文件,其实就是要根据该配置文件的内容,去创建 SqlSessionFactory
对象,然后再从中获取 SqlSession
对象来操作数据库:
1 | InputStream is = Resources.getResourceAsReader("mybatis-config.xml"); |
转换到 Spring 的语境下,其实就是要在 IoC 容器中注册一个 SqlSessionFactory
对象。因此 mybatis-spring
就直接提供了一个工厂 Bean 对象 SqlSessionFactoryBean
,用于辅助 Spring 创建 SqlSessionFactory
对象。
SqlSessionFactoryBean
提供了非常多的属性来帮助我们转移 MyBatis 核心配置文件中的配置。
比如 MyBatis 配置文件中的 <environment>
标签下的 <dataSource>
标签配置,就对应了 SqlSessionFactoryBean
的 dataSource
属性:在 Spring 配置文件下,我们可以先声明一个数据源如 DruidDataSource
,然后注入到 SqlSessionFactoryBean
的 dataSource
属性中。
细节二:<transactionManager type="JDBC"/>
的底层原理
<transactionManager>
用于配置 Mybatis 的事务管理器,属性 type
有两个可选值:JDBC
和 MANAGED
。
如果
type
为JDBC
的话,那么所构建的SqlSessionFacory
对象中所采用的TransactionFactory
就是JdbcTransactionFactory
,创建出来的SqlSession
底层采用的就是JdbcTransaction
来管理事务。如果
type
为MANAGED
的话,那么所构建的SqlSessionFacory
对象中所采用的TransactionFactory
就是ManagedTransactionFactory
,创建出来的SqlSession
底层采用的就是ManagedTransaction
来管理事务。
1 | // SqlSessionFactoryBuilder |
从源码可以看到,Mybatis 的事务管理器,其实指的是 SqlSessionFactory
所采用的事务工厂 TransactionFactory
。
细节三:MapperScannerConfigurer
的底层原理
回顾之前学习 Mybatis 的时候,我们是使用 sqlSession.getMapper(AccountMapper.class)
来获取接口的动态代理的。
而在 Spring + Mybatis 的整合场景下,我们没有显示地去编写 sqlSession.getMapper
的代码,而是通过在 Spring 配置文件中配置了一个 MapperScannerConfigurer
接口扫描配置器,让该配置器为我们生成接口代理并注册到 Spring IoC 容器中去。
1 | <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> |
下面将深入源码,说明 MapperScannerConfigurer
的底层的执行过程。
了解 BeanDefinitionRegistryPostProcessor
接口原理
我们先来关注 MapperScannerConfigurer
所实现的 BeanDefinitionRegistryPostProcessor
接口。
BeanDefinitionRegistryPostProcessor
接口的源码如下:
1 | public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor { |
接口中的 postProcessBeanDefinitionRegistry
方法是 Spring 框架所提供的一个扩展点:在所有的 BeanDefinition
注册到 Spring 以后,在 Spring 根据这些 BeanDefinition
来进行实例化以前,Spring 会调用这个方法,并传入保存了当前所有 BeanDefinition
的 BeanDefinitionRegistry
。
所以,如果你希望在所有的 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 | // MapperScannerConfigurer |
我们先来看方法的参数,Spring 调用该方法时传入的 registry
参数保存了当前所有已注册的 BeanDefinition
,此时打断点可以看到,目前 registry
中还不包含 Mapper 接口相关的 BeanDefiniton
。
方法内部首先创建了一个 Mapper 扫描器,并使用 MapperScannerConfigurer
类内部的成员变量,对该扫描器的一些参数进行初始化。
在 Spring 配置文件中声明 MapperScannerConfigurer
时,我们并没有为其 mapperFactoryBeanClass
属性提供值,该属性也没有默认初始化,所以 this.mapperFactoryBeanClass
为 null
,此时 scanner
就会使用一个默认的 Class 对象 MapperFactoryBean.class
来对内部的 mapperFactoryBeanClass
属性进行赋值,源码如下:
1 | public void setMapperFactoryBeanClass(Class<? extends MapperFactoryBean> mapperFactoryBeanClass) { |
扫描器创建并初始化配置完成后,就开始进行扫描了。Mapper 接口的扫描逻辑在 scanner.scan
方法中,打开该方法,发现主要的逻辑,有都放在了 doScan
方法内。
1 | public int scan(String... basePackages) { |
再来看 doScan
方法,该方法主要分为两步:
第一步:先扫描指定
basePackage
及其子包下的所有 Mapper 接口,为每个 Mapper 接口创建一个BeanDefinition
对象,并注册到 Spring 中。
假设basePackage
下有一个AccountMapper
接口,那么super.doScan()
执行完以后,Spring 的BeanDefinitionRegistry
中包含一个新的BeanDefinition
,其名字为accountMapper
,其对应的 Class 对象为AccountMapper.class
。第二步:每个 Mapper 接口本身作为接口,是无法实例化的,因此需要经过
processBeanDefinitions()
方法对每个 Mapper 接口对应的BeanDefinition
进行调整。
1 |
|
调整/修改阶段:MapperScannerConfigurer
对新创建 BeanDefinition
进行调整,确保 Spring 根据调整后的 BeanDefinition
所实例化的是一个 MapperFactoryBean
对象
processBeanDefinitions
方法内部具体是怎么调整的呢?
1 | private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { |
假设我们正在调整的是 AccountMapper
对应的 BeanDefinition
。
- 我们先来看调整二,这一步是最关键的一步。
调整前,BeanDefinition
中所关联的 Class 对象是接口 Class 对象 AccountMapper.class
,接口 Class 显然是无法进行实例化的,所以修改为 MapperFactoryBean.class
。这样后续 Spring 在根据调整后的 BeanDefinition
进行实例化时,创建的就是 MapperFactoryBean
对象了。
MapperFactoryBean
是一个实现了 FactoryBean
接口的工厂 Bean 对象,其内部有一个非常重要的 mapperInterface
属性,该属性绑定了某个 Mapper 接口的 Class 对象比如 AccountMapper.class
,后续将会以构造注入的方式赋值。
1 | public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { |
可以看到,MapperFactoryBean
的 getObject
方法就是为内部绑定的 Mapper 接口生成相应的接口代理。
- 我们再来看调整一,这一步主要是提供
MapperFactoryBean
中mapperInterface
属性的初始化参数,方便对其进行构造注入。
这里面有个小细节要说明一下,MapperFactoryBean
中的 mapperInterface
是 Class 类型,但是我们在调整一中所设置的初始化参数,却是有个全类名字符串,比如 com.powernode.bank.mapper.AccountMapper
,这能成功注入吗?
答案是可以的,因为 Class
类型是 Spring 中的简单类型,所以直接提供全类名字符串作为 value,Spring 底层会为我们做相应的类型转换,得到全类名对应的类的 Class 对象再进行注入!
- 我们最后来看调整三,这一步确保
MapperFactoryBean
继承自父类的sqlSessionTemplate
属性,后续能够通过按类型的自动装配附上值。
还记得在调整一中 getObject()
方法内部调用的 getSqlSession()
吗?其实这个是父类 SqlSessionDaoSupport
的方法。我们来看 SqlSessionDaoSupport
中比较关键的部分:
1 | public abstract class SqlSessionDaoSupport extends DaoSupport { |
因为 MapperFactoryBean
继承了 SqlSessionDaoSupport
,所以 MapperFactoryBean
类中也包含了继承自父类的 setSqlSessionFactory
方法。
在调整三处,我们将 BeanDefinition
的装配模式设置为按类型自动装配,这样后续 Spring 在实例化时,就会去查找 MapperFactoryBean
类中的所有 setter 方法并进行调用。
此时 setSqlSessionFactory
方法就得到调用,该方法根据传入的 sqlSessionFactory
,创建一个新的 SqlSessionTemplate
赋值给内部的 sqlSessionTemplate
属性,从而 getObject()
方法内部调用的 getSqlSession()
就能获取到值了!
从 MapperFactoryBean
到 Mapper 接口的代理类
当 Service 类需要注入相应的 Mapper 接口时,就会从三级缓存中找到该 Mapper 接口对应的 MapperFactoryBean
,然后调用 getObject()
方法生成最终的代理类,并注入到 Service 中。
之前我们说过 MapperFactoryBean
的 getObject()
方法内部,依然是调用 sqlSession.getMapper
来生成接口代理的:
1 | // MapperFactoryBean |
只不过,这里 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 | // SqlSessionFactoryBean |
在 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 | // AbstractPlatformTransactionManager |
- 开启事务的主要工作是:先从数据源中获取了一个
Connection
对象,关闭自动提交,然后将该连接放到一个ThreadLocal
中。
更具体地说,从数据源中获取的 Connection
对象会先被封装到一个 ConnectionHolder
对象中,然后关闭连接的自动提交,最后将 ConnectionHolder
放到 TransactionSynchronizationManager
类中的 ThreadLocal<Map<Object, Object>> resources
中。
1 | // DataSourceTransactionManager |
注意,resources
为线程绑定的是一个 Map<Object, Object>
集合。当前线程第一次进行绑定时,resources.get()
为空,因此会创建一个空的 Map 集合绑定进去。
然后将数据源作为 key,将 ConnectionHolder
作为 value,放入该 Map 集合,从而完成 Connection
到 ThreadLocal
的绑定。
1 | // TransactionSynchronizationManager |
我们已经通过 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 | public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { |
所以,问题的关键就是 getSqlSession
底层到底是怎么获取当前线程的 SqlSession
对象的。
先说明一下 getSqlSession
的第一个参数,SqlSessionTemplate
这个类中还包括一个 SqlSessionFactory
属性,该属性注入的就是 SqlSessionFactoryBean
的 getObject()
的结果,是一个 DefaultSqlSessionFactory
类型的对象。
SqlSessionTemplate
内置该属性的目的就是,当获取不到当前线程的 SqlSession
对象时,通过该属性可以去创建一个!所以,在上面的注释中,我说获取到当前线程的 SqlSession
对象是 DefaultSqlSession
类型的。
1 | public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { |
在第一阶段中,我们就是将当前线程的 Connection
封装为一个 ConnectionHolder
绑定到 resources
中。但是这里要获取的不是 ConnectionHolder
而是 SqlSessionHolder
,所以第一次执行时,这里 resources.get()
所得到的 Map<Object, Object>
集合中只包括 (dataSource, ConnectionHolder)
这一个键值对。
既然获取不到,我们就 sessionFactory.openSession()
创建一个新的 SqlSession
来返回。
在返回之前,我们还需要做一些工作:将新的 SqlSession
绑定到当前线程。具体看 registerSessionHolder
函数:
1 | private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { |
还记得我们在第一阶段,通过 Spring AOP 为当前线程绑定的 Connection
对象吧。
现在我们为当前线程绑定了两个对象:SqlSession
和 Connection
。但显然,这两个对象之间还没建立起关系,所谓的建立起关系,就是将 SqlSession
底层的 Connection
设置为当前线程绑定的 Connection
。
关系的建立被推迟到数据库操作的执行时。回到 SqlSessionInterceptor
的目标方法部分,目前我们已经获取到了一个 DefaultSqlSession
类型的对象,该对象底层使用的事务管理器是 SpringManagedTransactionFactory
。
由于还没有第一次执行数据库操作,当前的 DefaultSqlSession
底层的 SpringManagedTransactionFactory
中的 Connection
还是 null
。
当真正执行时,会发现该 Connection
为 null
,然后从 resources
获取之前绑定到当前线程的 ConnectionHolder
,从中取出 Connection
进行赋值。
1 | // SpringManagedTransactionFactory |
Mybatis 的事务管理器和 Spring 的事务管理器,底层所操作的 Connection
对象到这一步才终于达成了一致。
第四阶段:Spring AOP 在目标业务方法执行结束后,在后置增强中完成事务的提交、回滚与关闭
在 Spring 的事务管理中,业务方法作为 Spring AOP 的目标方法:
- 其前置增强,是 Spring 事务管理器创建并开启事务
- 其后置增强,是 Spring 事务管理器完成事务的提交、回滚与关闭
事务的提交、回滚与关闭,除了对底层的 Connection
对象执行 commit
rollback
和 close
以外,我们可能还需要做其他的一些操作,比如最容易想到的,在事务提交或回滚之前,从 resources
移除掉绑定到当前线程的 Connection
对象和 SqlSession
对象。
这些额外操作被称为事务同步操作,它们与事务本身的提交或回滚并不直接相关,它们可以在事务的不同阶段(事务的生命周期)被触发。
我们现在回过头来,应该就能明白第三阶段提到的,注册事务同步是什么意思了。
1 | private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { |
Spring 在将 SqlSession
绑定到当前线程之后,紧接着立刻为其创建了一个 SqlSessionSynchronization
事务同步器,并将该事务同步器也绑定到当前线程。只不过 SqlSession
是绑定到 resources
上,事务同步器是绑定到 TransactionSynchronizationManager
类中的另外一个 ThreadLocal
变量 synchronizations
上。
1 | // 为当前线程绑定 Connection 与 SqlSession |
PS:SqlSessionSynchronization
是 mybatis-spring
提供的事务同步器实现,其中的事务同步操作已经写好了。
在目标业务方法执行完成以后,Spring AOP 实现会在其后置增强部分完成两部分工作:
- 获取绑定到当前线程的
Connection
,执行commit
rollback
以及close
操作 - 获取绑定到当前线程的事务同步器,在
commit
方法执行前后触发一些事务同步操作,在rollback
方法执行前后触发一些事务同步操作等
Spring 的事务管理器中会对 Connection
做 commit
操作,Mybatis 的 SqlSession
其底层的事务也会做 commit
操作。那会不会出现重复 commit
的情况呢?
答案是不会,可以看一下 Mybatis 的事务管理器 SpringManagedTransaction
的源代码:
1 |
|
注意 if 语句的第二个条件 !this.isConnectionTransactional
,它表示检查当前连接对象是否被 Spring 事务所管理,如果是的话,将不会对 SqlSession
底层的 Connection
执行 commit
操作。
也就是说,Spring 的事务管理器会调用 Mybatis 的事务管理器去执行 commit
操作,只不过 Mybatis 事务管理器自己内部会拒绝提交,最终交给 Spring 的事务管理器来完成提交。