关于后端:发现Spring事务的一个实锤bug官方还拒不承认你来评评理

2次阅读

共计 11055 个字符,预计需要花费 28 分钟才能阅读完成。

事件是这样的,上周我正在全神贯注的摸鱼,而后有个小伙伴给我发来微信音讯,提出了本人对于事务的一个疑难,并配上两段代码:

先说论断:我认为这是 Spring 事务的一个 bug。然而官网说这只能算是文档上的缺点,不能算是代码的 bug。
(好吧,我这篇文章写了好几天,所以我写到下面这一句的时候,官网还不抵赖是 bug,然而写完之后他们也抵赖的确是代码缺点。不影响,接着往下看。)
好家伙,我懂了,所有解释权归官网所有。
在开始刨根问底之前,我想先就对于如何发问这个问题掰扯几句。
我把下面这个读者的问题截进去,是因为我感觉这个发问几乎就是模板办法般的发问。
给了一段示例代码、给了一段源码、阐明本人的问题、并表明本人曾经查问过然而没有找到适合的答案。
我读完他的文字,我很快就能 get 到他的问题是什么,所以咱们之间的交换就十分的高效。
最终咱们并没有探讨出一个正当的解释,于是他去提了一个 issues,心愿能失去官网的比拟权威的答复。
所以咱们的故事就围绕着这个 issues 开始吧。

舞台搭建
在正戏开始之前,我先给你把舞台搭建进去,也就是把 Demo 搞进去。
因为是对于 Spring 事务的问题嘛,所以这个 Demo 次要就是体现出“事务”的利用就行了嘛。
所以 Demo 外面最外围的货色就是这个局部:

其中波及到的两个异样就是简略的自定义异样:

假如这里有一个只容许 10-18 岁的用户应用奇怪的网站,这个局部就代表这个网站的用户注册性能。
接着咱们往 user_info 表外面插入一条数据,而后判断年龄如果大于 18 岁,那么抛出 AgeExceptionOver18 异样,示意这个用户不是指标用户。
然而你留神,我 @Transactional 注解外面的 rollbackFor 是 AgeException.class,意思是我并不想回滚这个大于 18 岁的用户,我还是想把他的注册信息保留下来,只是抛出一个异样来示意他不是我的指标用户,
而当咱们插入一个年龄小于 10 岁的用户的时候,会抛出 AgeException.class,应该把刚刚执行的插入语句给回滚掉,我并不想保留这部分用户的信息。
好的,那么当初就会有小伙伴问了:小于 10 岁的用户既然不想保留,那么为什么不在插入之前判断呢?
很好的问题,理论开发中必定是要在插入之前判断的,然而我这里只是为了演示事务性能,所以我就这样写了,咋地了吧!

下面的代码,我来搞个接口触发一下,也就是弄个 Controller 进去:

下面的四个类,就是最要害的几个类,所以我独自拿出来说一下。
整个我的项目构造也十分的简略:

其余的类不要害,就不拿出来说了,都是你最拿手的 crud。花五分钟搭一个这个我的项目进去不过分吧?两头还能摸两分钟的鱼。
我把日志级别调整为 debug 级别,接着把我的项目跑起来验证一下性能。
而后调用这个链接:

http://127.0.0.1:8085/insertU…

对应的日志是这样的:

能够看到我框起来的局部,首先确认执行了 insert 语句,且 age 的确是为 8。然而最初 Rolling back 了,即回滚了。
为什么回滚,咱们心里也是门清,因为这里响应上了:

接下来试一下 age 为 18 岁的用户:

http://127.0.0.1:8085/insertU…

对应的日志是这样的:

这没啥说的,事务胜利提交,数据插入胜利。是咱们预期的后果。
数据库数据也没故障:

而后试一下 age 为 28 岁的用户。
这个用户咱们的预期是抛出 AgeExceptionOver18 异样,然而数据得插入胜利。
来走一个:

http://127.0.0.1:8085/insertU…

对应的日志是这样的:

首先数据竟然回滚了???
异样倒是抛出来了,然而这也没响应上啊!

先不论到底啥原理吧,从我的认知来说,首先我的 @Transactional 注解用法相对没有错,事务配置没有相对没有错,我的异样也没有乱抛,你凭什么给我回滚了?
你还说这不是 bug?
just 改改 documentation 就行了?
话中有话是要“抵赖”,强行从文档上找补吗?

这不是欺侮老实人吗?

额 … 等等,我写这段的时候状况是这样的。
然而等我写完这段,第二天再次进 issues 外面去看,发现事件产生了变动,官网又抵赖这是一个 bug 了,会在 5.3.x 版本外面批改文档上的形容,会在 6.0 版本外面进行代码上的修复。

然而我后面曾经铺垫了这么多,曾经写好了,我就不改了,就当在这里留下一个创作痕迹吧,哈哈。
咱们接着往下看。
戏剧抵触
一部戏,必定有它的戏剧抵触,这是它的外围局部。那么咱们 Demo 外面的外围抵触是什么呢?
这一大节就先通知你“戏剧抵触”在哪。
我先问你一个问题:

Spring 治理的事务,默认回滚的异样是什么呢?

咱们带着这个问题去看源码,找到了这个问题的答案,你就能丝滑入戏。
先搞个断点,把程序跑起来,而后看调用栈:

能够看到调用栈外面和事务相干的有这样一个办法:

org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

这就是咱们的突破口。
什么,你问我怎么一下就找到了这里来的?
我只能说:游刃有余而已。
好吧,其实是有技巧的,你能够本人试着去找一下,因为这不是本文重点,所以我就不多说了。
办法执行异样之后,会走到 catch 代码块外面,上面这一行代码就是异样相干解决的入口:

在咱们 age=28 的这个场景下,这个办法进来之后,首先 ex 参数就是咱们自定义的 AgeExceptionOver18 异样:

我还框起来了一行代码:
txInfo.transactionAttribute.rollbackOn(ex)
复制代码
这一行代码你看名字 rollbackOn 也晓得是判断 ex 参数是否匹配指定的回滚异样。
如果匹配呢?如果不匹配呢?

如果匹配,rollback。
如果不匹配,commit。
好了,咱们接着往下看。
你会走到这里来:

org.springframework.transaction.interceptor.RuleBasedTransactionAttribute#rollbackOn

而这个办法,当 winner 为 null 的时候,下面有个正文,是说没有匹配到对应的规定。
也就是咱们什么都不配置,默认状况下,winner 就是 null。
那么上面这行代码外面就藏着咱们要找的问题的答案:

return super.rollbackOn(ex);

所以 Spring 治理的事务,默认回滚的异样是什么呢?
源码通知我:如果以后抛出的异样属于 RuntimeException 或者 Error 都会回滚。
后面都是我在铺路,只是为了把你引到 rollbackOn 办法这个中央来,甚至 super.rollbackOn(ex) 这行代码都是烟雾弹,本文中咱们齐全不用关注。

咱们须要关注的,是这个局部:

首先咱们明确一下这个 rollbackRules 是啥玩意。
在咱们的 Demo 外面,它就是咱们配置在 @Transactional 注解上 rollbackFor 属性中的 AgeException.class 对象的全门路名称:

好,接下来就要害了,你肯定要打起精神来。

重点关注这一行代码:

int depth = rule.getDepth(ex);

来,看一下 rule 和 ex 别离是什么货色:

rule 外面的 exceptionName 是咱们配置的 AgeException 对象的全门路名称。
ex 是咱们程序抛出的 AgeExceptionOver18 异样。
这场戏的外围抵触,就藏在这里的这个办法外面:

org.springframework.transaction.interceptor.RollbackRuleAttribute#getDepth(java.lang.Throwable)

好,至此,舞台搭建实现,外围抵触曾经裸露进去。
好戏筹备收场。

大幕拉开

你认真看我框起来的代码。
后面的 exceptionClass.getName() 是啥玩意?
它长这样:

前面的 this.exceptionName 是啥玩意?
它是这个玩意:

接下来,神奇的事件就要产生了,铁子。

com.example.transactional.exception.AgeExceptionOver18com.example.transactional.exception.AgeException

尽管这是两个不同的异样,然而这两个字符串进行 contains 操作,你说是不是返回 true?

所以,这里的 depth 会返回 0。
那么这里的 winner 不会为空

因而这个办法的返回值就会是 true:

还记得我后面说的吗,这里返回 true 会执行什么代码?
是不是就 rollback 回滚了?

所以,万恶之源就是咱们大幕拉开的时候就提到的这一段代码:

org.springframework.transaction.interceptor.RollbackRuleAttribute#getDepth(java.lang.Class<?>, int)

到这里,我感觉曾经十分明确了:这难道不是 bug 吗?你强如 Spring 难道还想诡辩?

然而,如果上面这两个字符串进行 equals 操作,你说是不是返回 false,问题就失去解决了?

com.example.transactional.exception.AgeExceptionOver18com.example.transactional.exception.AgeException

情理是这么个情理,然而我感觉问题必定没这么简略。
首先我感觉这里用 contains 必定是成心的,然而具体出于什么目标,我还真不确定。
于是和我探讨的读者提出一个认识,会不会是为了满足 rollbackForClassName 这个属性:

因为当咱们用 rollbackForClassName 的时候能够用字符串数组的模式去配置多个须要回滚的异样名称,比方我搞个 NullPointerException:

在失常应用的场景下,咱们是能够实现回滚操作的。
对应中央的代码的值是这样的;

java.lang.NullPointerException 字符串当然蕴含了 NullPointerException 字符串。所以咱们进行回滚嘛。没故障。
然而如果咱们用 equals 操作,那么就匹配不上,导致 rollbackForClassName 属性生效了。
所以把 contains 批改为 equals 属于拆西墙,补东墙的措施,不可取。
然而 rollbackForClassName 属性在咱们的 Demo 下,也是没有成果的。
比方我把程序改成这样,你说,是不是就乱套了?

同样的情理嘛。
com.example.transactional.exception.AgeExceptionOver18 字符串当然蕴含了 AgeException 字符串了。
然而我并不想回滚啊,哥,你好好看看,我抛出来的异样是 AgeExceptionOver18 呀。
到这里,我想问题我应该曾经形容的十分分明了,要是你还是没明确问题是什么,那你不必往下看了,再看一下“大幕拉开”这一节。
不然前面你很难入戏。

铺垫一波
为了把真正的问题更好的抛出来,我必须得先把另外一个相干的问题引出来,作为铺垫。
首先,咱们去 Spring 我的项目的 issues 外面搜一下 getDepth 办法所在的 RollbackRuleAttribute 这个类。
看看有没有相干的蛛丝马迹,后果如下:

通过剖析,对我有帮忙的也只有第一条内容。

github.com/spring-proj…

题目叫做:

Improve javadoc in RollbackRuleAttribute regarding nested classes。改良 RollbackRuleAttribute 中对于嵌套类的 javadoc。

从题目咱们晓得这是一次对于文档的改良。
那么具体是啥改良呢?

能够看到他的形容中也提到了咱们后面剖析的那一个“万恶之源”的办法。
对于他具体说了什么,其实我也不必给你翻译,间接给你看他提交的代码就高深莫测了:

他次要说个了外部类的问题,而且他这个问题和咱们的还有点不一样。
他的两个异样类,一个叫 EnclosingException,另一个叫做 EnclosedException,这两个字符串是不存在 contains 关系的。
那么在内部类的场景下,问题是什么呢?
我也给你演示一个,你只须要看一眼就明确了,示例代码如下:

须要留神的是,我当初的两个异样是 AgeException 和 AgeOver18Exception,这二者并不存在蕴含关系。
后面做 Demo 的时候是 AgeExceptionOver18。

AgeOver18ExceptionAgeExceptionOver18

别看花眼了。
你看,外部类的时候抛出异样是这样的:

throw new AgeException.AgeOver18Exception();

你要是没回过味儿来,没关系,断点一打,代码一跑就豁然开朗:

看明确没,铁子。外部类抛出的异样的全门路名称是这样的:

xxx.UserInfoService\ AgeException\AgeOver18Exception

这不就蕴含 AgeException 了吗,不就匹配上了吗,不就回滚了吗?
所以,尽管他这个问题的触发形式和我后面提到的还不一样,然而“万恶之源”是一样的。
那么解决方案是什么呢?
仅仅是批改了一下文档,从文档的角度表明了这个状况是会被回滚的:

对应到源码,也就是这个中央的正文:

org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(java.lang.Class<?>)

好,咱们当初沉着的思考一下,这里仅仅是从文档的角度来修复这个问题,在文档外面明确阐明指定异样的外部类也会被回滚,这个做法对不对?
我认为勉强是能够承受的。
比方,咱们晓得某个异样类被标记为应该被回滚,那么这个异样类的子类应该被回滚,这是没问题的。
我认为外部类和子类应该保留同样的逻辑,毕竟它们之前的确存在代码上的关联关系,从这个角度上也说的过来。
毕竟所有解释权归官网所有嘛。
到这里你要记住:

RollbackRuleAttribute 类曾经因为在回滚异样的判断上应用 contains 爆出过外部类的问题。
这个问题通过批改 javadoc 形容的形式进行了修复,没有批改任何代码。
这个解决方案勉强说的过来。

好了,铺垫实现了。
好戏演出
我再次在 issues 外面搜寻 RollbackRuleAttribute,会发现多了一条内容:

后面其实我刻意把这一条内容给暗藏了,因为这个 issues 就是和我聊天的读者提的。

这里的示例代码就是文章最结尾呈现的代码。

好戏就藏在这个 issues 外面的,一起看一下官网是怎么“重复横跳”。

github.com/spring-proj…

首先,是一个叫做 snicoll 的哥们把这个 issues 的题目改了一下:

去掉了结尾的“Bug”,这也很失常,属于发问不规范,现阶段只是提问者认为是一个 bug,你这样取名字,集体主观意识太过强烈,这样不好。
次要是你晓得批改题目的 snicoll 是谁吗?

别问,问就是大佬,Spring 和 Spring Boot 我的项目外围保护人员。
而后隔了几天,这个问题的题目又被另外一个大佬,简略的批改了一下:

仅仅是把 use contains 批改为了 uses contains(),把 equals 批改为了 equals()。
这个小细节完满的体现了 Spring 框架的谨严之处,能够说是十分的谨严了。
还没进入到问题解答环节,先把问题的“错别字”给批改了。

接着就进入了官网答疑环节。
说了上面这么大一堆内容,然而基本不要慌,你晓得的我的 English level 是十分的 high 的。这一堆内容分为三大部分,我会一点点的给你说明确:

首先是第一局部:

一上来就是个英语长句,然而基本不要怕。
你看他先是简明扼要的提到了一个短语“by design”,也就是“设计如此”。
整个翻译过去大略就是这样的。
这个中央咱们要用 contains() 办法呢?这其实是通过思考的。
那么是基于什么思考呢?
在 XML 配置文件中,用户通常指定自定义异样类型的简略名称,而不是全门路类名。
啥意思呢?
他给了一个文档中的 xml 配置示例:

那我当初基于咱们的 Demo 也搞一个 xml 配置嘛,回到若干年前的基于 xml 配置的形式:

这里我也不得不感叹一句:以前基于 xml 开发的时候是真的麻烦,每次都要去零碎我的项目外面拷一份配置进去,所以我还是很感激 SpringBoot 的呈现的。
这里他想表白一个什么意思呢?
在我的 xml 配置中,对于 rollback-for 属性。他提到的 simple name 就是 AgeException。而 fully qualified class name 就是 com.example.transactional.exception.AgeException。
就是说这里是不限度用户填什么的。
如果用户填的是 simple name,咱们也应该让其失效,所以必须要应用 contains() 办法。
以我了解,这个中央和 @Transactional 注解外面的 rollbackForClassName 属性的用法是一样,而这是一个历史遗留问题,是当年一个不好的设计。
然而我认为不能说考虑不周,毕竟他人也很难想到你会依照那么奇怪的形式去命名异样类啊!
总之这一段话他解释了为什么会用 contains() 办法,为什么不能用 equals() 办法。
和咱们后面剖析的基本一致,只是咱们没有想到 XML 的配置形式。
第二段,他开始从文档的角度来解释这个问题。

叫咱们关注一下 RollbackRuleAttribute 上的 Javadoc 形容。
这里有一个“NB”,不是咱们经常说的牛逼,而是一个缩写:

你看,又在我这里学到一个用不上的英文常识。
咱们接着看,次要关注我划线的两句。
第一句是说:因为应用的 contains() 办法,“Exception”简直能够匹配任何规定,并且可能会暗藏其余规定。然而“java.lang.Exception”这个全门路的字符串,那么匹配范畴就小了很多了。
第二句是说:因为应用的 contains() 办法,对于“BaseBusinessException”等不寻常的异样名称,不须要应用类的全门路名称。
所以,第二段他想表白的是:文档上咱们曾经说过了,对于匹配规定,要认真思考,要十分的小心:

u1s1,他的确写了,然而你感觉你会看吗?
第三段就很简略了:

看到 its subclasses, and its nested classes. 就晓得这是咱们后面“铺垫一波”大节说过的局部。
所以你当初晓得我为什么给你铺垫了吧?
如果不给你铺垫一波,你忽然看到一个外部类的单词 nested classes,你说你一下反馈得过去吗?
你要永远置信我的行文构造。

好了,当初看另外一句我标注的中央,翻译过去是说:在以后的实现中最初一句话并没有恪守。
这里的“最初一句话”就是指 RollbackRuleAttribute 的 Javadoc 的最初一句,也就是 … its subclasses, and its nested classes 这句。
当然没恪守了。
我写的 Demo 外面的两个异样即不存在子类父类的关系,也不存在外部类的关系。
所以我感觉很纳闷:这个 Javadoc 和我的问题之间并不存在关系,或者说并不抵触啊。后面我也说了,对于这部分的 Javadoc 我感觉是没有故障的。如果你想要从批改文档的角度来解决这个问题,也不应扯到子类,外部类啥的,应该是齐全另起一行才对。
然而具体怎么解决,他并没有立刻表态,而是把这个 issues 放到了 Triage Queue 外面:

Queue,队列,你必定都晓得。
Triage 是个啥?
我也不晓得,于是我也学到了一个新单词:

也就是说官网把这个 issues 放到了“待分类的”一个队列外面,阐明他目前是理解到了问题的所在,然而具体应该怎么解决,还没有定论,有待商讨。
隔了一天这个老哥又来表态了,开始“横跳”:

他说他又想了一下,须要更正他之前的说法:RollbackRuleAttribute(Class) 构造函数的 Javadoc 是 mostly correct,也就是根本没故障的。须要改良的是对于回滚规定上的形容。
总之他还是想从文档的角度来修复这个问题。
然而解释了我后面的纳闷:即便从批改文档的角度来解决这个问题,也不应扯到子类,外部类啥的,应该是齐全另起一行才对。
他这里的“回滚规定”也就是“另起一行”。
接着,他对工作的状态进行了流转:

从“待分类”挪动到了“文档”的标签下。
而后示意在 5.3.17 这个里程碑版本中会进行修复:

同时,再次批改了 issues 的题目:

Transaction rollback rules may result in unintentional matches for similarly named exceptions 事务回滚规定可能会导致无心中匹配到名称类似的例外情况

其实如果让我来解决这个问题,我大概率也是会从文档的角度动手,并且最多加一点揭示日志,毕竟这是你应用不标准导致的。
而且我文档上曾经阐明有“坑”了,你本人没看踩进去了,这怪不得我呀。
然而在和这个读者表白了我的观点之后,他提出的不一样的认识:

他感觉使用者大多并不关注日志,主张抛出异样的形式进行强揭示:
于是他在 issues 上表白了本人的认识:

他感觉须要更精准的匹配规定,大多数人是不看文档的。
接着官网驳回了他的意见,并把该需要挪动到了 6.0.0-M3 这个里程碑的版本中去实现:

他的具体回复如下:

他说:老铁,我批准你对于“须要更精准的匹配规定”的观点。
咱们会修复 5.3.x 的文档形容。
而后在 6.0 版本中,咱们会改良一版代码。
具体来说是这样的:

如果异样模式是以字符串模式提供的。例如,在 XML 配置中或通过 @Transactional(rollbackForClassName = “example.CustomException”) 配置,那么现有的 contains() 逻辑将持续被应用。
如果一个具体的异样类型是以类援用的模式提供的。例如,通过 @Transactional(rollbackFor = example.CustomException.class),将会有新的逻辑。它齐全采纳配置上提供的类型信息,从而防止了在 example.CustomException(没有 2)被提供为异样类型时,与 example.CustomException2 的意外匹配。

他这里提到的 CustomException 和 CustomException2,其实是他的测试用例外面的代码。类比于咱们后面的 AgeException 和 AgeExceptionOver18 这两个异样。
接着,他对这个 issues 进行了从新分类,从“文档”类型,批改为了“enhancement”类型:

enhancement,是个四级词语,背一下,会考:

示意这个 issues 须要通过批改代码来使健壮性更强。
而后再次批改了题目:

对于事务回滚规定,应该应用异样的类型信息,而不是用模式匹配。
原本故事到这里都曾经是大结局了,我写到这里的时候就筹备收尾了。
想着收尾不焦急,先睡一觉再说。
后果 …
第二天早上起来,他!又!更!新!了!
我还得补一段内容。

最初一集
早上起来,我一刷新页面,发现官网针对这个 issues 进行了最初一次提交:

这次 issues 的题目,最初定格为:

Support type-safe transaction rollback rules 反对类型平安的事务回滚规定

而这次对应的代码提交链接是这样的:

github.com/spring-proj…

外面写了很长一段的内容,来形容这次提交的背景,然而基本上都是我后面写过的货色的总结:

联合我后面写的货色,我给你翻译翻译:
首先我感觉是在事务模块外面发明一个新的概念:type-safe rollback rules,类型平安的回滚规定。
在这次提交之前,只有一种事务回滚机制,Pattern-based rollback rules,即基于匹配模式的回滚规定。
而官网说基于匹配模式的回滚规定,会带来三种意料之外的匹配状况:

不同包中的雷同命名的异样类,会被意外匹配上。比方,example.client.WebException 和 example.server.WebException 都会与“WebException”模式匹配。
在同一个包中有相似命名的异样,这里说的类似是指当一个给定的异样名称是以另一个异样的名称结尾时。例如:example.BusinessException 和 example.BusinessExceptionWithDetails 都与“example.BusinessException”模式匹配。
嵌套异样,也就是当一个异样类被申明在另一个异样类外面的时候。例如:example.BusinessException 和 example.BusinessException$NestedException 都会与“example.BusinessException”匹配上。

第一种没啥说的,请应用全门路名称去防止。
第二种就是咱们文章中的例子,须要通过批改代码解决。
第三种外部类的状况我也在后面铺垫过了。然而过后的解决方案是仅减少文档中对应的形容。
然而当初,你看他怎么说的:

这次的提交能够避免后两种状况的意料之外的匹配。也就是说这次提交不仅修复了咱们的问题,还修复了外部类的问题。
那么怎么修复的呢?
首先是在 RollbackRuleAttribute 类外面新增了一个 exceptionType 字段:

而后在构造方法外面对其进行赋值:

外围代码变成了这样:

当 exceptionType 字段,即全门路名称不为空的时候,应用 equals() 办法,也就是 type-safe rollback rules。否则应用 contains() 办法,也就是 Pattern-based rollback rules。
另外,对于 Javadoc 上的很多形容也产生了变动,我就不一一举例了,强烈建议你本人去看看这次提交。
我只特地拿出一处变动来给你看看:

去掉了“外部类”,改成了“类型平安”。
至此,问题失去解决。
然而在 XML 外面或者用 @Transactional 注解外面的 rollbackForClassName 属性,也就是应用匹配模式的时候,还是会有意料之外的匹配状况。
官网:反正我在文档上说分明了,你要是还踩坑,那就怪得不我了?
最初,再插一个对于编程标准的事儿。
你想这次这个问题齐全是因为你有两个这样的异样类名称引起的:

AgeExceptionAgeExceptionOver18

而对于异样类,咱们都约定成俗的要求必须以“Exception”结尾。
包含阿里巴巴 Java 开发手册在命名格调外面也特意提到了这一点,且是强制要求:

所以,如果咱们都恪守这个规定,大家就相安无事。
那么,这个故事最初通知咱们一个什么情理呢?
它通知咱们 …

它通知咱们规定就是拿来突破的,如果你不突破规定,永远也踩不到这个坑,也就不会推动 Spring 的改变。
突破规定,这是你的一小步,却是开源世界的一大步。
所以,兄弟们,铁子们,不要按部就班,要突破 …

正文完
 0