好戏上演
我再次在 issues 里面搜索 RollbackRuleAttribute,会发现多了一条内容:
好戏就藏在这个 issues 里面的,一起看一下官方是怎么“反复横跳”。
https://github.com/spring-projects/spring-framework/issues/28098
首先,是一个叫做 snicoll 的哥们把这个 issues 的标题改了一下:
别问,问就是大佬,Spring 和 Spring Boot项目核心维护人员。
然后隔了几天,这个问题的标题又被另外一个大佬,简单的修改了一下:
仅仅是把 use contains
修改为了 uses contains()
,把 equals
修改为了 equals()
。
这个小细节完美的体现了 Spring 框架的严谨之处,可以说是非常的严谨了。
还没进入到问题解答环节,先把问题的“错别字”给修改了。
接着就进入了官方答疑环节。
说了下面这么大一堆内容,但是根本不要慌,你知道的我的 English level 是非常的 high 的。这一堆内容分为三大部分,我会一点点的给你说明白:
一上来就是个英语长句,但是根本不要怕。
你看他先是简明扼要的提到了一个短语“by design”,也就是“设计如此”。
整个翻译过来大概就是这样的。
这个地方我们要用 contains()
方法呢?这其实是经过考虑的。
那么是基于什么考虑呢?
在 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 里面:
也就是说官方把这个 issues 放到了“待分类的”一个队列里面,说明他目前是了解到了问题的所在,但是具体应该怎么解决,还没有定论,有待商榷。
隔了一天这个老哥又来表态了,开始“横跳”:
他说他又想了一下,需要更正他之前的说法:RollbackRuleAttribute(Class) 构造函数的 Javadoc 是 mostly correct,也就是基本没毛病的。需要改进的是关于回滚规则上的描述。
总之他还是想从文档的角度来修复这个问题。
但是解释了我前面的疑惑:即使从修改文档的角度来解决这个问题,也不应扯到子类,内部类啥的,应该是完全另起一行才对。
他这里的“回滚规则”也就是“另起一行”。
接着,他对任务的状态进行了流转:
Transaction rollback rules may result in unintentional matches for similarly named exceptions
事务回滚规则可能会导致无意中匹配到名称相似的例外情况
其实如果让我来处理这个问题,我大概率也是会从文档的角度入手,并且最多加一点提醒日志,毕竟这是你使用不规范导致的。
而且我文档上已经说明有“坑”了,你自己没看踩进去了,这怪不得我呀。
但是在和这个读者表达了我的观点之后,他提出的不一样的看法:
他觉得使用者大多并不关注日志,主张抛出异常的方式进行强提醒:
于是他在 issues 上表达了自己的看法:
他说:老铁,我同意你关于“需要更精准的匹配规则”的观点。
我们会修复 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”类型:
对于事务回滚规则,应该使用异常的类型信息,而不是用模式匹配。
本来故事到这里都已经是大结局了,我写到这里的时候就准备收尾了。
想着收尾不着急,先睡一觉再说。
结果...
第二天早上起来,他!又!更!新!了!
我还得补一段内容。
最后一集
早上起来,我一刷新页面,发现官方针对这个 issues 进行了最后一次提交:
这次 issues 的标题,最后定格为:
Support type-safe transaction rollback rules
支持类型安全的事务回滚规则
而这次对应的代码提交链接是这样的:
https://github.com/spring-projects/spring-framework/commit/c1033dbfb3609f3b3fe002d7b582b3302620c05a
里面写了很长一段的内容,来描述这次提交的背景,但是基本上都是我前面写过的东西的总结:
结合我前面写的东西,我给你翻译翻译:
首先我觉得是在事务模块里面创造一个新的概念: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” 匹配上。
第一种没啥说的,请使用全路径名称去避免。
第二种就是我们文章中的例子,需要通过修改代码解决。
第三种内部类的情况我也在前面铺垫过了。但是当时的解决方案是仅增加文档中对应的描述。
但是现在,你看他怎么说的:
当 exceptionType 字段,即全路径名称不为空的时候,使用 equals()
方法,也就是type-safe rollback rules。否则使用 contains()
方法,也就是 Pattern-based rollback rules。
另外,关于 Javadoc 上的很多描述也发生了变化,我就不一一举例了,强烈建议你自己去看看这次提交。
我只特别拿出一处变化来给你看看:
去掉了“内部类”,改成了“类型安全”。
至此,问题得到解决。
但是在 XML 里面或者用 @Transactional 注解里面的 rollbackForClassName 属性,也就是使用匹配模式的时候,还是会有意料之外的匹配情况。
官方:反正我在文档上说清楚了,你要是还踩坑,那就怪得不我了?
最后,再插一个关于编程规范的事儿。
你想这次这个问题完全是因为你有两个这样的异常类名称引起的:
AgeException
AgeExceptionOver18
而对于异常类,我们都约定成俗的要求必须以“Exception”结尾。
包括阿里巴巴 Java 开发手册在命名风格里面也特意提到了这一点,且是强制要求:
它告诉我们规则就是拿来打破的,如果你不打破规则,永远也踩不到这个坑,也就不会推动 Spring 的改动。
打破规则,这是你的一小步,却是开源世界的一大步。
所以,兄弟们,铁子们,不要墨守成规,要打破...
最后,文章首发于公众号[why技术],欢迎关注,第一时间接收最新文章。