背景介绍
一个老项目,数据库用的是 MySQL 5.7.36,ORM 框架用的 MyBatis 3.5.0,mysql-connector-java 版本是 5.1.26。
新来了一个干练的小伙,精力充沛,看着就是个喜欢折腾的主。
他觉得 MyBatis 用起来不够顺手,要写的代码还不少,于是琢磨着把它替换成 MyBatis-Plus。
MyBatis-Plus 替换 MyBatis
先准备一张表 tbl_order,然后初始化 2 条数据。

为了简化演示,直接用 MyBatis-Plus 搭一个示例 demo,来模拟“小伙”当时的替换过程。
替换范围只在 MyBatis 这一层,其他组件版本暂时不动。MyBatis-Plus 版本就用“小伙”引用的 3.1.1,mysql-connector-java 继续保持 5.1.26。
示例代码:playitsafe
此时运行 com.qsl.OrderTest#orderListAllTest,直接报错,异常信息如下。
注意看 Caused by:

不支持的转换类型:java.time.LocalDateTime。
谁不支持?mysql-connector-java 不支持!
那 mysql-connector-java 从哪个版本开始支持了?答案是 5.1.37。

升级 mysql-connector-java
把 mysql-connector-java 升级到 5.1.37,重新执行 com.qsl.OrderTest#orderListAllTest。

不再报异常,查询结果也正确。
MyBatis-Plus 替换 MyBatis 似乎就这样完成了,顺利得让人有点怀疑。
Conversion not supported for type java.time.LocalDateTime
回过头再琢磨一下前面那个异常:Conversion not supported for type java.time.LocalDateTime。
替换之前没人报这个,替换之后就冒出来了——难道这是 MyBatis-Plus 的问题?
要找根因很简单,直接从异常堆栈入手。

点进去之后,你会发现这个方法简单得不能再简单了。

这么简单的代码能出什么问题?
注意看图中左上角 MyBatis 的版本,是 3.5.1,并不是最初的 3.5.0。
有人可能想问了:不是用 MyBatis-Plus 替换了 MyBatis 吗?怎么还有 MyBatis?
这个问题问得真好,我只想给你个大嘴巴子。
你瞅一眼 MyBatis-Plus 的官方说明就明白了。

既然基于 MyBatis 3.5.0 没抛异常,基于 3.5.1 却抛了,那 LocalDateTimeTypeHandler 在 3.5.1 里肯定动了手脚。
来看看到底改了什么:

看出问题了吗?
MyBatis 3.5.0 会自己处理 LocalDateTime 类型的转换,也就是把 java.sql.Timestamp 转成 java.time.LocalDateTime。
然而,注意了,然而来了!!!
从 MyBatis 3.5.1 开始,它不再处理 LocalDateTime(还有 LocalDate、LocalTime)的转换了,转而交给 JDBC 组件,也就是 mysql-connector-java 去实现。
巧就巧在,mysql-connector-java 5.1.26 偏偏不支持 LocalDateTime。
那它支持哪些类型?我们还是从异常堆栈往里钻。


点进去之后,能看到下面这张图。

往上滑动鼠标,就能看到支持的类型列表了。
确实没有 LocalDateTime、LocalDate 和 LocalTime。
而 mysql-connector-java 5.1.37 开始支持这几个时间类型,前面已经提过,不重复了。
小结一下异常根因:MyBatis 3.5.1 开始不再处理 LocalDateTime、LocalDate 和 LocalTime 的转换,而 mysql-connector-java 5.1.37 之前的版本都不支持这些类型。
把这个来龙去脉捋顺之后,前面的“顺利”是不是又显得有些理所当然了?
暴风雨的来临
版本上线还没两天,该来的还是来了。
给表 tbl_order 里插一条新记录:
INSERT INTO tbl_order VALUES (3, 'asdfgh', NULL, '2024-02-21 20:01:31.111', '2024-02-21 20:02:56.764');
再跑一遍 com.qsl.OrderTest#orderListAllTest。

此时我就想问“小伙”一句:刺不刺激?
碰到异常,继续找原因,还是从异常堆栈入手。
如果 getTimestamp(columnIndex) 拿到的是 NULL,可不就 NullPointerException 了?这代码的严谨性跑哪去了?
修复要紧,先看看哪个版本把这个问题修掉了。

把 mysql-connector-java 升级到 5.1.42。

问题解决。
经此一役,“小伙”看起来是成长了不少,但眼里的光却暗淡了一些。
mybatis-plus-issues-1114
无意中翻到了这个 issue-1114,跟前面分析的 Conversion not supported for type java.time.LocalDateTime 是不是同一个问题?
只是我们这里默认用的连接池是 HikariCP,而不是 Druid。
结合 druid/issues/3302 来看,如果使用 Druid 作为数据库连接池,出现的异常可能跟前面分析的确实不一样。
所以还是要根据自己的实际情况来分析,不过异常排查的方法论是通用的。
修了“不该修的 Bug”
这是我亲身经历的一次事故,到现在都觉得这锅背得有点冤。

文件分为主文件和附属文件,主文件生成之后,再生成附属文件。
附属文件生成时,会校验它依赖的主文件是否都生成了:只要有任意一个主文件没生成,依赖文件就不能生成并抛出异常。
这个业务逻辑不算复杂吧?
但就在附属文件校验的优化上,我硬生生背上了一个生产事故。
优化前的校验

listFileGenerateLog 的作用是根据参数查询文件生成记录,具体实现不用管。
这个校验逻辑是什么意思?只要有任意一个主文件生成,校验就算通过。可业务要求的是“主文件全部生成才算通过”呀。
这不是妥妥的 Bug 吗?
优化后的校验
碰到 Bug 你能忍?我是忍不了一点,反手就是一个优化。

这下是不是就符合业务要求了?
生产异常
中午更新上线之后,系统稳定运行了一段时间,文件也都正常生成,没出什么幺蛾子。
结果晚上 19 点,有个附属文件生成失败,异常提示:依赖的资源 abc_{yyyyMMdd}.txt 未生成。
第一眼看到这个异常,感觉既熟悉又陌生。熟悉的是异常信息的结构,陌生的是 abc_{yyyyMMdd}.txt——这不是文件名吗?
正常情况下这里应该是 fileId,一个自增的正整数,怎么会变成文件名?
脑子里瞬间闪过一个念头:数据库数据有问题?
一查吓一跳,这个附属文件关联主文件的字段值居然是:4356,abc_{yyyyMMdd}.txt,最终修改时间是 2021-08-21 15:22:12.652。
4356 文件的文件名就是 abc_{yyyyMMdd}.txt,正常来说,这个关联字段的值应该是 4356。
敢情这个校验 Bug 一直完美地“兼容”了这条脏数据,所以几年下来从没出过异常。
是不是有点那个味儿了?
这可倒好,我把 Bug 修好了,反而冒出问题。你说我是不是手贱?
经此一役,我眼里的光又暗淡了些许。
总结
对组件的升级、对旧代码的调整,都可能牵一发动全身,影响面比你想的大得多。
我的观点很直白:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑。
如果真到了非改不可的地步,那就必须做足全面测试。云栈社区里也常有类似的升级踩坑记录分享,建议动手之前多逛逛、多看看,把前人踩过的坑先扫一遍,能少交不少学费。