你好呀,我是歪歪。
是的,正如题目形容的这样,我试图通过这篇文章,教会你如何浏览源码。
事件大略是这样的,前段时间,我收到了一个读者发来的相似于这样的示例代码:
他说他晓得这三个案例的回滚状况是这样的:
- insertTestNoRollbackFor:不会回滚
- insertTestRollback:会回滚
- insertTest:会回滚
他说在没有执行代码之前,他也晓得前两个为什么一个不会回滚,一个会回滚。因为抛出的异样和 @Transactional 外面的注解响应上了。
然而第三个到底会不会回滚,没有执行之前,他不晓得为什么会回滚。执行之后,回滚了,他也不晓得为什么回滚了。
我通知他:源码之下无机密。
让他去看看这部分源码,了解它的原理,不然这个中央抛出一个其余的异样,又不晓得会不会回滚了。
然而他说他齐全不会看源码,找不到下手的角度。
所以,就这个问题,我打算写这样的一篇文章,试图教会你一种浏览源码的形式。让你找到一个好的切入点,或者说突破口。
然而须要当时阐明的是,浏览源码的形式十分的多,这篇文章只是站在我集体的角度介绍浏览源码的泛滥形式中的一种,沧海一粟,就像是一片树林外面的一棵树的树干上的一叶叶片的叶脉中的一个小分叉而已。
对于啃源码这件事儿,没有一个所谓的“一招吃遍天下”的秘诀,如果你非要让我给出一个秘诀的话,那么就只有一句话:
啃源码的过程,肯定是十分干燥的,特地是啃本人接触不多的框架源码的时候,千头万绪,也得下手去捋,所以肯定要耐得住寂寞才行。
而后,如果你非得让我再补充一句的话,那么就是:
调试源码,肯定要亲!自!动!手!只是去看相干的文章,而没有本人一步步的去调试源码,那你相当于看了个寂寞。
亲自动手的第一步就是搞个 Demo 进去。用“黑话”来说,这个 Demo 就是你的抓手,有了抓手你能力打出一套实践结合实际的组合拳。抓手多了,就能积淀出可复用的方法论,最终为本人赋能。
搭建 Demo
所以,第一步必定是先把 Demo 给搭建起来,我的项目构造十分的简略,规范的三层构造:
次要是一个 Controller,一个 Service,而后搞个本地数据库给接上,就齐全够够的了:
Student 对象是从表外面映射过去的,轻易弄了两个字段,次要是演示用:
就这么一点代码,给你十分钟,你是不是就能搭建好了?两头甚至还能摸几分钟鱼。
要是只有这么一点货色的、极其简略的 Demo 你都不想本人亲自动手搭一下,而后本人去调试的话,仅仅是通过阅读文章来肉眼调试,那么我只能说:
在正式开始调试代码之前,咱们还得明确一下调试的目标:想要晓得 Spring 的 @Transactional 注解对于异样是否应该回滚的判断逻辑具体是怎么样的。
带着问题去调试源码,是最容易有播种的,而且你的问题越具体,播种越快。你的问题越抽象,就越容易在源码外面迷失。
方法论之关注调用栈
本人 Debug 的过程就是一直的打断点的过程。
我再说一次:本人 Debug 的过程就是一直的打断点的过程。
打断点大家都会打,断点打在哪些地方,这个玩意就很考究了。
在咱们的这个 Demo 下,第一个断点的地位十分好判断,就打在事务办法的入口处:
一般来说,大家调试业务代码的时候,都是顺着断点往下调试。然而当你去浏览框架代码的时候,你得往回看。
什么是“往回看”呢?
当你的程序在断点处停下的时候,你会发现 IDEA 外面有这样的一个局部:
这个调用栈是你在调试的过程中,一个十分十分十分重要的局部。
它示意的是以以后断点地位为起点的程序调用链路。
为了让你彻底的明确这句话,我给你看一张图:
我在 test6 办法中打上断点,调用栈外面就是以 test6 办法为起点到 main 办法为终点的程序调用链接。
当你去点击这个调用栈的时候,你会发现程序也会跟着动:
“跟着动”的这个动作,你能够了解为你站着断点处“往回看”的过程。
当你了解了调用栈是干啥的了之后,咱们再具体看看在以后的 Demo 下,这个调用栈外面都有写啥:
标号为 ① 的中央,是 TestController 办法,也就是程序的入口。
标号为 ② 的中央,从包名称能够看出是 String AOP 相干的办法。
标号为 ③ 的中央,就能够看到是事务相干的逻辑了。
标号为 ④ 的中央,是以后断点处。
好,到这里,我想让你简略的回顾一下你来调试代码的目标是什么?
是不是想要晓得 Spring 的 @Transactional 注解对于异样是否应该回滚的判断逻辑具体是怎么样的。
那么,咱们是不是应该次要把关注的重点放在标号为 ③ 的中央?
也就是对应到这一行:
这个中央我肯定要特地的强调一下:要放弃指标清晰,很多人在源码外面迷失的起因就是人不知; 鬼不觉间被源码牵着走远了。
比方,有人看到标号为 ② 的局部,也就是 AOP 的局部,一想着这玩意我眼生啊,书上写过 Spring 的事务是基于 AOP 实现的,我去看看这部分代码吧。
当你走到 AOP 外面去的时候,路就开始有点走偏了。你明确我意思吧?
即便在这个过程中,你翻阅了这部分的源码,的确理解到了更多的对于 AOP 和事务之间的关系,然而这个局部并不解决你“对于回滚的判断”这个问题。
然而更多更实在的状况可能是这样的,当你点到 AOP 这部分的时候,你一看这个类名称是 CglibAopProxy:
你一细嗦,Cglib 你也相熟啊,它和 JDK 动静代理是一对好兄弟,都是老八股了。
而后你可能又会点击到 AopProxy 这个接口,找到 JdkDynamicAopProxy:
接着你豁然开朗:哦,我在什么都没有配置的状况下,以后版本的 SpringBoot 默认应用的是 Cglib 作为动静代理的实现啊。
诶,我怎么记得我背的八股文默认是应用 JDK 呢?
网上查一下,查一下。
哦,原来是这么一回事儿啊:
- SpringBoot 1.x,默认应用的是 JDK 动静代理。
- SpringBoot 2.x 开始,为了解决应用 JDK 动静代理可能导致的类型转化异样而默认应用 CGLIB。
- 在 SpringBoot 2.x 中,如果须要默认应用 JDK 动静代理能够通过配置项 spring.aop.proxy-target-class=false 来进行批改,proxyTargetClass 配置已有效。
刚刚提到了一个 spring.aop.proxy-target-class 配置,这是个啥,咋配置啊?
查一下,查一下 …
喂,醒一醒啊,敌人,走远了啊。还记得你调试源码的目标吗?
如果你对于 AOP 这个局部感兴趣,能够先进行简略的记录,然而不要去深刻的追踪。
不要感觉本人只是轻易看看,不要紧。反正正是因为这些“轻易看看”导致你在源码外面忙了半天感觉这波学到了,然而停下来一想:我 TM 刚刚看了些啥来着?我的问题怎么还没解决?
我为什么要把这部分十分详尽,甚至于靠近啰嗦的写一遍,就是因为这个就是初看源码的敌人最容易犯的谬误。
特别强调一下:抓住主要矛盾,解决次要问题。
好,回到咱们通过调用栈找到的这个和事务相干的办法中:
org.springframework.transaction.interceptor.TransactionInterceptor#invoke
这个办法,就是咱们要打第二个断点,或者说这才是真正的第一个断点的中央。
而后,重启我的项目,从新发动申请,从这个中央就能够进行正向的调试,也就是从框架代码一步步的往业务代码执行。
比方这个办法接着往下 Debug,就来到了这个中央:
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction
找到了这个中央,你就算是有限的靠近于问题的假相了。
这个局部我必定会讲的,然而在这里先按下不表,毕竟这并不是本文最重要的货色。
本文最重要的是,我再次重申一遍:我试图想要教会你一种浏览源码的形式,让你找到一个好的切入点,或者说突破口。
因为这个案例比较简单,所以很容易找到真正的第一个利于调试的断点。
如果遇到一些简单的场景、响应式的编程、异步的调用等等,可能会周而复始的执行下面的动作。
剖析调用栈,打断点,重启。
再剖析调用栈,再打断点,再重启。
方法论之死盯日志
其实我发现很少有人会去留神框架打印的日志,就像是很少有人会去仔细阅读源码上的 Javadoc 一样。
然而其实通过观察日志输入,也是一个很好的寻找浏览源码突破口的形式。
咱们要做的,就是保障 Demo 尽量的单纯,不要有太多的和本次排查无关的代码和依赖引入。
而后把日志级别批改为 debug:
logging.level.root=debug
接着,就是发动一次调用,而后耐着性子去看日志。
还是咱们的这个 Demo,发动一次调用之后,控制台输入了很多的日志,我给你搞个缩略图看看:
咱们已知的是这外面大概率是有线索的,有没有什么办法尽量快的找进去呢?
有,然而通用性不强。所以如果教训不够丰盛的话,那么最好的办法就是一行行的去找。
后面我也说过了:啃源码的过程,肯定是十分干燥的。
所以你肯定会找到这样的日志输入:
Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction
Switching JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] to manual commit
...
==> Preparing: insert into student (name, home) values (?, ?)
HikariPool-1 - Pool stats (total=1, active=1, idle=0, waiting=0)
==> Parameters: why(String), 草市街 199 号 -insertTestNoRollbackFor(String)
<== Updates: 1
...
Committing JDBC transaction on Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c]
Releasing JDBC Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] after transaction
这几行日志,不就是正对应着 Spring 事务的开启和提交吗?
有了日志,咱们齐全能够基于日志去找对应的日志输入的中央,比方咱们当初要找这一行日志输入对应的代码:
o.s.j.d.DataSourceTransactionManager : Acquired Connection [HikariProxyConnection@982684417 wrapping com.mysql.cj.jdbc.ConnectionImpl@751a148c] for JDBC transaction
首先,咱们能够依据日志晓得对应输入的类是 DataSourceTransactionManager 这个类。
而后找到这个类,依照关键词搜寻:
不就找到这一行代码了吗?
或者咱们间接秉承鼎力出奇观的真谛,来一个暴力的全局搜寻,也是能搜到这一行代码的:
再或者批改一下日志输入格局,把行号也搞进去嘛。
当咱们把日志格局批改为这样之后:
logging.pattern.console=%d{dd-MM-yyyy HH:mm:ss.SSS} %magenta([%thread]) %highlight(%-5level) %logger.%M:%L – %msg%n
控制台的日志就变成了这样:
org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin:263 – Acquired Connection [HikariProxyConnection@1569067488 wrapping com.mysql.cj.jdbc.ConnectionImpl@19a49539] for JDBC transaction
很直观的就看进去了,这行日志是 DataSourceTransactionManager 类的 doBegin 办法,在 263 行输入的。
而后你找过来,发现没有任何故障,这就是案发现场:
我后面给你说这么多,就是为了让你找到这一行日志输入的中央。
当初,找到了,而后呢?
而后必定就是在这里打断点,而后重启程序,从新发动调用了啊。
这样,你又能失去一个调用栈:
而后,你会从调用栈中看到一个咱们相熟的货色:
敌人,这不就和后面写的“方法论之关注调用栈”响应起来了吗?
这不就是一套组合拳吗,不就是积淀出的可复用的方法论吗?
黑话,咱们也是能够整两句的。
方法论之查看被调用的中央
除了后面两种办法之外,我有时候也会间接看我要浏览局部的办法,在框架中被哪些地方调用了。
比方在咱们的 Demo 中,咱们要浏览的代码十分的明确,就是 @Transactional 注解。
于是间接看一下这个注解在哪些地方用到了:
有的时候调用的中央会十分的少,甚至只有一两处,那么间接在调用的中央打上断点就对了。
尽管 @Transactional 注解一眼望去也是有很多的调用,然而认真一看大多是测试类。排除测试类、JavaDoc 外面的备注和本人我的项目中的应用之后,只剩下很显著的这三处:
看起来很靠近假相,然而很遗憾,这里只是在我的项目启动的时候解析注解而已。和咱们要调研的中央,差的还有点远。
这个时候就须要一点教训了,一看苗头不对,立马转换思路。
什么是苗头不对呢?
你在这几个中央打上断点了,只是在我的项目启动的过程中断点起作用了,发动调用的时候并没有在断点处停下,阐明发动调用的时候并不会触发这部分逻辑,苗头不对。
顺着这个思路想,在我的 Demo 中抛出了异样,那么 rollbackFor 和 noRollbackFor 这两个参数大概率是会在调用的时候被用到,对吧?
所以当你去看 rollbackFor 被调用的时候只有咱们本人写的业务代码在调用:
怎么办呢?
这个时候就要靠一点运气了。
是的,靠运气。
你都点到 rollbackFor 这个办法来了,你也看了它被调用的中央,在这个过程中你大概率会瞟到几眼它对应的 JavaDoc:
org.springframework.transaction.annotation.Transactional#rollbackFor
而后你会发现在 JavaDoc 外面提到了 rollbackOn 这个办法:
org.springframework.transaction.interceptor.DefaultTransactionAttribute.rollbackOn(Throwable)
到这里一看,你发现这是一个接口,它有好多个实现类:
怎么办呢?
晚期的时候,因为不晓得具体的实现类是哪个,我是在每个实现类的入口处都打上断点,尽管是笨办法,然而总是能起作用的。
起初我才发现,原来能够间接在接口上打断点:
而后,重启我的项目,发动调用,第一次会停在咱们办法的入口:
F9,跳过以后断点之后,来到了这个中央:
这里就是我后面在接口上打的办法断点,走到了这个实现类中:
org.springframework.transaction.interceptor.DelegatingTransactionAttribute
而后,要害的就来了,咱们又有一个调用栈了,又从调用栈中看到一个咱们相熟的货色:
敌人,组合拳这不又打起来了?突破口不就又找到了?
对于“瞟到几眼对应的 JavaDoc,而后就可能找到突破口”的这个景象,晚期对我来说的确是运气,然而当初曾经是一个习惯了。一些出名框架的 JavaDoc 真的写的很分明的,外面暗藏了很多要害信息,而且是最权威的正确信息,读官网文档,比读技术博客稳当的多。
摸索答案
后面我介绍的都是找到代码调试突破口的办法。
当初突破口也有了,接下来应该怎么办呢?
很简略,调试,重复的调试。从这个办法开始,一步一步的调试:
org.springframework.transaction.interceptor.TransactionInterceptor#invoke
如果你真的想要有所播种的话,这是一个须要你亲自去入手的步骤,必须要有逐行浏览的一个过程,而后能力晓得大略的解决流程。
我就不进行具体解读了,只是把重点给大家画一下:
框起来的局部,就是去执行业务逻辑,而后基于业务逻辑的处理结果,去走不同的逻辑。
抛异样了,走这个办法:completeTransactionAfterThrowing
失常执行结束了,走这个办法:commitTransactionAfterReturning
所以,咱们问题的答案就藏在 completeTransactionAfterThrowing 外面。
持续调试,进入这个办法之后,能够看到它拿到了事务和以后异样相干的信息:
在这个办法外面,大体的逻辑是当标号为 ① 的中央为 true 的时候,就在标号为 ② 的中央回滚事务,否则就在标号为 ③ 的中央提交事务:
因而,标号为 ① 的局部就很重要了,这外面就藏着咱们问题的答案。
另外,在这里多说一句,在咱们的案例中,这个办法,也就是以后调试的办法是不会回滚的:
而这个办法是会回滚的:
也就是这两个办法在这个中央会走不同的逻辑,所以你在调试的时候遇到 if-else 就须要留神,去构建不同的案例,以笼罩尽量多的代码逻辑。
持续往下调试,会进入到标号为 ① 的 rollbackOn 办法外面,来到这个办法:
org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn
这里,就藏着问题的终极答案,而且这外面的代码逻辑绝对比拟的绕。
外围逻辑就是通过循环 rollbackRules,这外面装的是咱们在代码中配置的回滚规定,在循环体中拿 ex,也就是咱们程序抛出的异样,去匹配规定,最初抉择一个 winner:
如果 winner 为空,则走默认逻辑。如果是 RuntimeException 或者是 Error 的子类,就要进行回滚:
如果有 winner,判断 winner 是否是不必回滚的配置,如果是,则取反,返回 false,示意不进行回滚:
那么问题的冠军就在于:winner 怎么来的?
答案就藏着这个递归调用中:
一句话形容就是:看以后抛出的异样和配置的规定中的 rollbackFor 和 noRollbackFor 谁间隔更近。这里的间隔指的是父类和子类之间的关系。
比方,还是这个案例:
咱们抛出的是 RuntimeException,它间隔 noRollbackFor=RuntimeException.class 为 0。RuntimeException 是 Exception 的子类,所以间隔 rollbackFor = Exception.class 为 1。
所以,winner 是 noRollbackFor,能明确吧?
而后,咱们再看一下这个案例:
依据后面的“间隔”的剖析,NullPointerException 是 RuntimeException 的子类,它们之间的间隔是 1。而 NullPointerException 到 Exception 的间隔是 2:
所以,rollbackFor=RuntimeException.class 这个的间隔更短,所以 winner 是 rollbackFor。
而把 winner 放到这个判断中,返回是 true:
return !(winner instanceof NoRollbackRuleAttribute);
所以,这就是它为什么会回滚的起因:
好了,到这里你有可能是晕的,晕就对了,去调试这部分代码,亲自摸一遍,你就搞的明明白白了。
最初,再给“死盯日志”的方法论打个补丁吧。
后面我说了,日志级别调整到 Debug 兴许会有意外发现。当初,我要再给你说一句,如果 Debug 没有查到信息,能够试着调整到 trace:
logging.level.root=trace
比方,当咱们调整到 trace 之后,就能够看到“winner 到底是谁”这样的信息了:
当然了,trace 级别下日志更多了。
所以,来,再跟我大声的读一遍:
啃源码的过程,肯定是十分干燥的,特地是啃本人接触不多的框架源码的时候,千头万绪,也得下手去捋,所以肯定要耐得住寂寞才行。
作业
我后面次要是试图教你一种浏览源码时,寻找突破点的技能。这个突破点,说白了就是第一个无效的断点到底应该打在哪里。
你用后面我教的办法,也能把 @Cacheable 和 @Async 都玩明确。因为它们的底层逻辑和 @Transactional 是一样的。
所以,当初安排两个作业。
拿着这套组合拳,去上手玩一玩 @Cacheable 和 @Async 吧,积淀出属于本人的方法论。
@Cacheable:
@Async:
最初,再附上几个我之前写过的文章,外面也用到了后面提到的几个办法定位源码,老难受了。有趣味能够看看:
《我是真没想到,这个面试题竟然从 11 年前就开始探讨了,而官网往年才表态。》
《的确很优雅,所以我要扯下这个注解的神秘面纱。》
《对于 Request 复用的那点破事儿。钻研明确了,给你汇报一下。》
《千万千万不要在办法上打断点!太坑了!》
好了,本文就到这里啦。如果你感觉对你有一丝丝帮忙的话,求个收费的赞,不过分吧?