关注Java极客技术,后台回复“java”,获取Java知识体系/面试必看资料
吐血推荐
今天,正式介绍一下Java极客技术知识星球
SpringBoot 精髓之 SpringBoot-starter
Spring 源码学习(八) AOP 使用和实现原理
Java:前程似锦的 NIO 2.0
我们谈谈面试技巧(初入职场年轻人该学的)
![7a8e08df72cfd2acf02736c46fa005c3.png](https://i-blog.csdnimg.cn/blog_migrate/b9c62b7438f858d65032d4fe9e4c283f.png)
业务系统的数据,一般最后都会落入到数据库中,例如 MySQL
、Oracle
等主流数据库,不可避免的,在数据更新时,有可能会遇到错误,这时需要将之前的数据更新操作撤回,避免错误数据。
Spring
的声明式事务能帮我们处理回滚操作,让我们不需要去关注数据库底层的事务操作,可以不用在出现异常情况下,在 try / catch / finaly 中手写回滚操作。
Spring
的事务保证程度比行业中其它技术(例如 TCC
/ 2PC
/ 3PC
等)稍弱一些,但使用 Spring
事务已经满足大部分场景,所以它的使用和配置规则也是值得学习的。
接下来一起学习 Spring
事务是如何使用以及实现原理吧。
使用例子
1.创建数据库表
create
2.创建对应数据库表的 PO
public
3.创建表与实体间的映射
在使用 JdbcTemplate
时很纠结,在 Java
类中写了很多硬编码 SQL
,与 MyBatis
使用方法不一样,为了示例简单,使用了 JdbcTemplate
,不过还是建议朋友们用 MyBatis
,让代码风格整洁。
public
4.创建数据操作接口
public
5.创建数据操作接口实现类
跟书中例子不一样,没有在接口上加入事务注解,而是在公共方法上进行添加,可以在每个方法上自定义传播事件、隔离级别。
public
6.创建配置文件
<?xml version="1.0" encoding="UTF-8"?>
7.添加依赖
记得添加数据库连接和 jdbc
、tx
这两个 spring
模块的依赖
optional(project(":spring-jdbc")) // for Quartz support
optional(project(":spring-tx")) // for Quartz support
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.6'
8.启动代码
public
通过上面的代码,我做了两个测试:
配置文件中,没开启事务。 也就是
这一行被注释了,虽然我们执行的方法中抛出了
RuntimeExcepton
,但是数据库中依旧被插入了数据。配置文件中,开启事务。 将上面的注释去掉,删掉数据库中的记录,重新执行启动代码,发现数据没有被插入, 在程序抛出异常情况下,
Spring
成功执行了事务,回滚了插入操作。
注解属性 @Transactional
具体位置在:org.springframework.transaction.annotation.Transactional
属性 | 类型 | 作用 |
---|---|---|
value | String | 可选的限定描述符,指定使用的事务管理器 |
propagation | 枚举:Propagation | 可选的事务传播行为 |
isolation | 枚举:Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 设置读写或只读事务,默认是只读 |
rollbackFor | Class 数组,必须继承自 Throwable | 导致事务回滚的异常类数组 |
rollbackForClassName | 类名称数组,必须继承自 Throwable | |
noRollbackFor | Class 数组,必须继承自 Throwable | 不会导致事务回滚的异常类数组 |
noRollbackForClassName | 类名称数组,必须继承自 Throwable |
事务的传播性 Propagation
REQUIRED
这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。
PROPAGATION_SUPPORTS
如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。
PROPAGATION_NOT_SUPPORTED
以非事务方式运行,如果当前存在事务,则把当前事务挂起。
PROPAGATION_NEVER
以非事务方式运行,如果当前存在事务,则抛出异常。
事务的隔离性 Isolation
READ_UNCOMMITTED
最低级别,只能保证不读取
物理上损害的数据,允许脏读
READ_COMMITTED
只能读到已经提交的数据
REPEATABLE_READ
可重复读
SERIALIZABLE
串行化读,读写相互阻塞
这里只是简单描述了一下这两个主要属性,因为底层与数据库相关,可以看下我之前整理过的 MySQL锁机制
Spring 中实现逻辑
介绍完如何使用还有关键属性设定,本着知其然,知其所以然的学习精神,来了解代码是如何实现的吧。
解析
之前在解析自定义标签时提到,AOP
和 TX
都使用了自定义标签,按照我们上一篇 AOP
的学习,再来一遍解析自定义标签的套路:事务自定义标签。
定位到 TxNamespaceHandler
类的初始化方法:
@Override
根据上面的方法,Spring
在初始化时候,如果遇到诸如 开头的配置后,将会使用
AnnotationDrivenBeanDefinitionParser
解析器的 parse
方法进行解析。
public BeanDefinition parse(Element element, ParserContext parserContext) {
Spring
中的事务默认是以 AOP
为基础,如果需要使用 AspectJ
的方式进行事务切入,需要在 mode
属性中配置:
<tx:annotation-driven mode="aspectj"/>
本篇笔记主要围绕着默认实现方式,动态 AOP
来学习,如果对于 AspectJ
实现感兴趣请查阅更多资料~
注册 InfrastructureAdvisorAutoProxyCreator
与 AOP
一样,在解析时,会创建一个自动创建代理器,在事务 TX
模块中,使用的是 InfrastructureAdvisorAutoProxyCreator
。
首先来看,在默认配置情况下,AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext)
做了什么操作:
private
在这里注册了代理类和三个 bean
,这三个关键 bean
支撑了整个事务功能,为了待会更好的理解这三者的关联关系,我们先来回顾下 AOP
的核心概念:
Pointcut
定义一个切点,可以在这个被拦截的方法前后进行切面逻辑。Advice
用来定义拦截行为,在这里实现增强的逻辑,它是一个祖先接口org.aopalliance.aop.Advice
。还有其它继承接口,例如MethodBeforeAdvice
,特定指方法执行前的增强。Advisor
用来封装切面的所有信息,主要是上面两个,它用来充当Advice
和Pointcut
的适配器。
![eaa80b0888fcfc7b55a6ca452db379b5.png](https://i-blog.csdnimg.cn/blog_migrate/cee208732602e0ea62599dfbe6773ee8.png)
回顾完 AOP
的概念后,继续来看下这三个关键 bean
:
TransactionInterceptor: 实现了
Advice
接口,在这里定义了拦截行为。AnnotationTransactionAttributeSource:封装了目标方法是否被拦截的逻辑,虽然没有实现
Pointcut
接口,但是在后面目标方法判断的时候,实际上还是委托给了AnnotationTransactionAttributeSource.getTransactionAttributeSource
,通过适配器模式,返回了Pointcut
切点信息。TransactionAttributeSourceAdvisor: 实现了
Advisor
接口,包装了上面两个信息。
这三个 bean
组成的结构与 AOP
切面环绕实现的结构一致,所以先学习 AOP
的实现,对事务的了解会有所帮助
接着看我们的自动创建代理器是如何创建的:
AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element)
public static void registerAutoProxyCreatorIfNecessary(
ParserContext parserContext, Element sourceElement) {
在这一步中,注册了一个 beanName
是 org.springframework.aop.config.internalAutoProxyCreator
的 bean
:InfrastructureAdsivorAutoProxyCreator
,下图是它的继承体系图:
![6a98ea5b8cf74f95230ca25bf234d9f1.png](https://i-blog.csdnimg.cn/blog_migrate/c920f8fb09e56a301b22e0a9f97f24a7.jpeg)
可以看到,它实现了 InstantiationAwareBeanPostProcessor
这个接口,也就是说在 Spring
容器中,所有 bean
实例化时,Spring
都会保证调用其 postProcessAfterInitialization
方法。
与上一篇介绍的 AOP
代理器一样,在实例化 bean
的时候,调用了代理器父类 AbstractAutoProxyCreator
的 postProcessAfterInitialization
方法:
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
其中关于 wrapIfNecessory
方法,在上一篇 AOP
中已经详细讲过,这里讲下这个方法做了什么工作:
找出指定 `bean` 对应的增强器
根据找出的增强器创建代理
与创建 AOP
代理相似的过程就不再重复说,讲下它们的不同点:
判断目标方法是否适合 canApply
AopUtils#canApply(Advisor, Class, boolean)
public static boolean canApply(Advisor advisor, Class> targetClass, boolean hasIntroductions) {
我们在前面看到,TransactionAttributeSourceAdvisor
的父类是 PointcutAdvisor
,所以在目标方法判断的时候,会取出切点信息 pca.getPointcut()
。
我们之前注入的切面类型 bean
是 AnnotationTransactionAttributeSource
,通过下面的方法包装,最后返回对象类型是 TransactionAttributeSourcePointcut
的切点信息
private
匹配标签 match
在匹配 match
操作中,区别的是 AOP
识别的是 @Before
、@After
,而我们的事务 TX
识别的是 @Transactional
标签。
判断是否是事务方法的入口方法在这:
org.springframework.transaction.interceptor.TransactionAttributeSourcePointcut#matches
@Override
那它到底到哪一步解析事务注解的呢,继续跟踪代码,答案是:
AnnotationTransactionAttributeSource#determineTransactionAttribute
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
在这一步中,遍历注册的注解解析器进行解析,由于我们关注的是事务解析,所以直接定位到事务注解的解析器:
SpringTransactionAnnotationParser#parseTransactionAnnotation(AnnotatedElement)
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
首先判断是否含有 @Transactional
注解,如果有的话,才继续调用 parse
解析方法:
protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
小结
通过上面的步骤,完成了对应类或者方法的事务属性解析。
主要步骤在于寻找增强器,以及判断这些增强器是否与方法或者类匹配。
如果某个 bean
属于可被事务增强时,也就是适用于增强器 BeanFactoryTransactionAttributeSourceAdvisor
进行增强。
之前我们注入了 TransactionInterceptor
到 BeanFactoryTransactionAttributeSourceAdvisor
中,所以在调用事务增强器增强的代理类时,会执行 TransactionInterceptor
进行增强。同时,也就是在 TransactionInterceptor
类中的 invoke
方法中完成整个事务的逻辑。
运行
事务增强器 TransactionInterceptor
TransactionInterceptor
支撑着整个事务功能的架构。跟之前 AOP
的 JDK
动态代理 分析的一样,TransactionInterceptor
拦截器继承于 MethodInterceptor
,所以我们要从它的关键方法 invoke()
看起:
public Object invoke(MethodInvocation invocation) throws Throwable {
实际调用了父类的方法:TransactionAspectSupport#invokeWithinTransaction
protected Object invokeWithinTransaction(Method method, @Nullable Class> targetClass,final InvocationCallback invocation) throws Throwable {
贴出的代码有删减,简略了错误异常的 try / catch
和编程式事务处理的逻辑。因为我们更多使用到的是声明式事务处理,就是在 XML
文件配置或者 @Transactional
注解编码,实际通过 AOP
实现,而编程式事务处理是通过 Transaction Template
实现,比较少使用到,所以省略了这部分处理代码。
事务管理器
通过该方法,确定要用于给定事务的特定事务管理器
TransactionAspectSupport#determineTransactionManager
protected PlatformTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
由于最开始我们在 XML
文件中配置过 transactionManager
属性,所以该方法在我们例子中将会返回类型是 DataSourceTransactionManager
的事务管理器,下面是 DataSourceTransactionManager
的继承体系:
![4ab4f7b4a460241089d073722299dff9.png](https://i-blog.csdnimg.cn/blog_migrate/00fc19a52d909e86d26a0231a3187557.png)
它实现了 InitializingBean
接口,不过只是在 afterPropertiesSet()
方法中,简单校验 dataSource
是否为空,不细说这个类。
事务开启
TransactionAspectSupport#createTransactionIfNecessary
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
在创建事务方法中,主要完成以下三件事:
使用 `DelegatingTransactionAttribute` 包装 `txAttr` 实例
获取事务:`tm.getTransaction(txAttr)`
构建事务信息:`prepareTransactionInfo(tm, txAttr, joinpointIdentification, status)`
核心方法在第二点和第三点,分别摘取核心进行熟悉。
获取 TransactionStatus
status = tm.getTransaction(txAttr);
由于代码较长,直接来总结其中几个关键点
获取事务
创建对应的事务实例,我们使用的是 DataSourceTransactionManager
中的 doGetTransaction
方法,创建基于 JDBC
的事务实例。
protected Object doGetTransaction() {
其中在同一个线程中,判断是否有重复的事务,是在 TransactionSynchronizationManager.getResource(obtainDataSource())
中完成的,关键判断逻辑是下面这个:
private
结论:resources
是一个 ThreadLocal
线程私有对象,每个线程独立存储,所以判断是否存在事务,判断的依据是当前线程、当前数据源(DataSource)中是否存在活跃的事务 - map.get(actualKey)
。
处理已经存在的事务
根据前面说的,判断当前线程是否存在事务,判断依据为当前线程记录的连接不为空且连接中(connectionHolder)中的 transactionActive
属性不为空,如果当前线程存在事务,将根据不同的事务传播特性进行处理。具体代码逻辑如下:
if (isExistingTransaction(transaction)) {
PROPAGATION_NEVER
在配置中配置设定为 PROPAGATION_NEVER
,表示该方法需要在非事务的环境下运行,但处于事务处理的状态(可能是外部带事务的方法调用了非事务的方法),将会抛出异常:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
PROPAGATION_NOT_SUPPORTED
如果有事务存在,将事务挂起,而不是抛出异常:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
事务挂起
对于挂起操作,主要目的是记录原有事务的状态,以便于后续操作对事务的恢复:
实际上,suspend()
方法调用的是事务管理器 DataSourceTransactionManager
中的 doSuspend()
方法
protected Object doSuspend(Object transaction) {
最后调用的关键方法是 TransactionSynchronizationManager#doUnbindResource
private static Object doUnbindResource(Object actualKey) {
看了第七条参考资料中的文章,结合代码理解了事务挂起的操作:移除当前线程、数据源活动事务对象的一个过程
那它是如何实现事务挂起的呢,答案是在 doSuspend()
方法中的 txObject.setConnectionHolder(null)
,将 connectionHolder
设置为 null
。
一个 connectionHolder
表示一个数据库连接对象,如果它为 null
,表示在下次需要使用时,得从缓存池中获取一个连接,新连接的自动提交是 true
。
PROPAGATION_REQUIRES_NEW
表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则这个方法运行期间被挂起。
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 新事务的建立
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
与前一个方法相同的是,在 PROPAGATION_REQUIRES_NEW
广播特性下,也会使用 suspend
方法将原事务挂起。方法 doBegin()
,是事务开启的核心。
PROPAGATION_NESTED
表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像 PROPAGATION_REQUIRES_NEW
。
在代理处理上,有两个分支,与 PROPAGATION_REQUIRES_NEW
相似的不贴出来,讲下使用 savepoint
保存点的方式事务处理:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
学习过数据库的朋友应该清楚 savepoint
,可以利用保存点回滚部分事务,从而使事务处理更加灵活和精细。跟踪代码,发现创建保存点调用的方法是 org.hsqldb.jdbc.JDBCConnection#setSavepoint(java.lang.String)
,感兴趣的可以往下继续深入学习~
事务创建
其实在前面方法中,都出现过这个方法 doBegin()
,在这个方法中创建事务,顺便设置数据库的隔离级别、timeout
属性和设置 connectionHolder
:
DataSourceTransactionManager#doBegin
protected void doBegin(Object transaction, TransactionDefinition definition) {
结论:Spring 事务的开启,就是将数据库自动提交属性设置为 false
小结
在声明式的事务处理中,主要有以下几个处理步骤:
获取事务的属性:
tas.getTransactionAttribute(method, targetClass)
加载配置中配置的 `TransactionManager`:
determineTransactionManager(txAttr);
不同的事务处理方式使用不同的逻辑:关于声明式事务和编程式事务,可以查看这篇文章-Spring编程式和声明式事务实例讲解
在目标方法执行前获取事务并收集事务信息:
createTransactionIfNecessary(tm, txAttr, joinpointIdentification)
执行目标方法:
invocation.proceed()
出现异常,尝试异常处理:
completeTransactionAfterThrowing(txInfo, ex);
提交事务前的事务信息消除:
cleanupTransactionInfo(txInfo)
提交事务:
commitTransactionAfterReturning(txInfo)
事务回滚 & 提交
这两步操作,主要调用了底层数据库连接的 API
,所以没有细说。
总结
本篇文章简单记录了如何使用 Spring
的事务,以及在代码中如何实现。
在之前的使用场景中,只用到了默认配置的声明式事务 @Transactional
,不了解其它属性设置的含义,也不知道在默认配置下,如果是同一个类中的方法自调用是不支持事务。
所以,经过这一次学习和总结,在下一次使用时,就能够知道不同属性设置能解决什么问题,例如修改广播特性 PROPAGATION
,让事务支持方法自调用,还有设置事务超时时间 timeout
、隔离级别等属性。
由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正
Gitee 地址 :
https://gitee.com/vip-augus/spring-analysis-note.git
Github 地址 :
https://github.com/Vip-Augus/spring-analysis-note
End
如有收获,请帮忙转发,您的鼓励是作者最大的动力!
![7a8e08df72cfd2acf02736c46fa005c3.png](https://i-blog.csdnimg.cn/blog_migrate/b9c62b7438f858d65032d4fe9e4c283f.png)
![c004a39883aa486dbb960e65ccd5ed42.png](https://i-blog.csdnimg.cn/blog_migrate/cc81932131fde18e4595af3b92eead78.jpeg)
我是本周的小编「沉默王二」,现在要隆重地告诉大家一个好消息,对于加入「Java极客技术」知识星球的同学提供基本的福利:
文章有疑问的地方可以提问,其他工作问题都可以提问出来,作者免费作答。
https://t.zsxq.com/Y3fYny7
每周都有大牛分享一些面试题,和面试注意的知识点!
https://t.zsxq.com/2bufE2v
每周由Java极客技术独家编制的设计模式与大家分享!
https://t.zsxq.com/3bUNbEI
每两周还会分享一个话题,和大家一起成长!
https://t.zsxq.com/BI6Unm2
还有Java极客技术团队亲自录制了一套 Spring Boot 视频,这套视频加密,加密后放到云盘上,下载链接加密之后,一机一码,每个星球的用户一个播放授权码。
我们做知识星球的目的和其他星主一样,就是为了帮助大家一起更好的成长,与高手拉近距离,减少差距,其实你也是高手!
![ec1b8a12a532e8cd62b50c609cb2c767.png](https://i-blog.csdnimg.cn/blog_migrate/56af432fa6a4d6a5aa83ff6194edd7c2.png)
![05f6a3937646d84623a3621d4b679544.png](https://i-blog.csdnimg.cn/blog_migrate/ccc4eeebdc3ccf2b4186b18273f2769b.jpeg)
![b7c0c0434b69c6967553cf5780b42cd9.png](https://i-blog.csdnimg.cn/blog_migrate/0a93bac365cc7d56ca748f7d4c4a3eed.jpeg)
![a786fc84ef9d028362e5f05c3ce3d8dc.png](https://i-blog.csdnimg.cn/blog_migrate/b96a3f2d75cd4ab99f8127e087ed1017.jpeg)
![9db25c4af3560989b308c45500e96621.png](https://i-blog.csdnimg.cn/blog_migrate/bcbc8e38a216bd4dc882ca7ca7a43fc2.jpeg)
前1000人,50元/每年,现在大约还剩407名额。
长按二维码
![7a8e08df72cfd2acf02736c46fa005c3.png](https://i-blog.csdnimg.cn/blog_migrate/b9c62b7438f858d65032d4fe9e4c283f.png)
欢迎长按下图关注公众号Java极客技术,后台回复“资料”,获取作者独家秘制精品资料
Java 极客技术公众号,是由一群热爱 Java 开发的技术人组建成立,专注分享原创、高质量的 Java 文章。如果您觉得我们的文章还不错,请帮忙赞赏、在看、转发支持,鼓励我们分享出更好的文章。