
一个使用 Java 技术栈的老项目,数据库是 MySQL 5.7.36,ORM 框架用的是 MyBatis 3.5.0,mysql-connector-java 的版本是 5.1.26。新来的同事觉得 MyBatis 使用起来不够便捷,代码量较多,于是决定将其替换为宣称能简化开发的 MyBatis-Plus。
替换过程与初次异常
首先,我们准备一张订单表并初始化数据。
DROP TABLE IF EXISTS `tbl_order`;
CREATE TABLE `tbl_order` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_no` varchar(50) NOT NULL COMMENT '订单号',
`pay_time` datetime(3) DEFAULT NULL COMMENT '付款时间',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB COMMENT = '订单';
INSERT INTO `tbl_order` VALUES (1, '123456', '2024-02-21 18:38:32.000', '2024-02-21 18:37:34.000', '2024-02-21 18:40:01.720');
INSERT INTO `tbl_order` VALUES (2, '654321', '2024-02-21 19:33:32.000', '2024-02-21 19:32:12.020', '2024-02-21 19:34:03.727');

为了简化演示,我们搭建一个示例工程来模拟替换过程。仅将 MyBatis 替换为 MyBatis-Plus 3.1.1,其他组件版本暂时不变,mysql-connector-java 保持 5.1.26。
此时运行查询测试,程序立刻抛出了异常:
org.springframework.dao.TransientDataAccessResourceException: Error attempting to get column 'pay_time' from result set. Cause: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
...
Caused by: java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1078)
at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:989)
...
at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5126)
at com.mysql.jdbc.JDBC4ResultSet.getObject(JDBC4ResultSet.java:547)
...
at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:38)

核心异常信息很明确:mysql-connector-java 不支持 java.time.LocalDateTime 类型的转换。那么哪个版本开始支持呢?答案是 5.1.37。
首次“解决”:升级驱动
将 mysql-connector-java 升级到 5.1.37 后,再次执行测试。

测试通过,查询结果正确。替换工作看似顺利完成,顺利得让人有些不安。

深入排查异常根源
我们回过头分析那个异常:Conversion not supported for type java.time.LocalDateTime。为何替换前(MyBatis 3.5.0)正常,替换后(引入了 MyBatis-Plus 及其依赖的 MyBatis 3.5.1)就出错了呢?
从异常堆栈入手,定位到 LocalDateTimeTypeHandler 的 getNullableResult 方法。

代码非常简单,只是调用了 ResultSet.getObject(columnName, LocalDateTime.class)。等等,这里 MyBatis 的版本是 3.5.1,不是项目原来的 3.5.0。MyBatis-Plus 是基于 MyBatis 的增强工具,自然会引入特定版本的 MyBatis 依赖。
那么,MyBatis 3.5.0 和 3.5.1 在这个处理器上有什么不同呢?

对比一目了然:
- MyBatis 3.5.0:主动处理
LocalDateTime 类型的转换,内部通过 getTimestamp 获取 java.sql.Timestamp,再转换为 java.time.LocalDateTime。
- MyBatis 3.5.1:不再处理转换,直接委托给 JDBC 驱动(即
mysql-connector-java)的 ResultSet.getObject 方法去处理。
而巧合(或者说必然)的是,项目原有的 mysql-connector-java 5.1.26 恰好不支持 LocalDateTime 类型。查看其 getObject 方法源码,也确实没有对 LocalDateTime、LocalDate、LocalTime 等 Java 8 时间类型的支持。

根因小结:MyBatis 从 3.5.1 版本开始,将 java.time.* 类型的转换职责下放给 JDBC 驱动。而 mysql-connector-java 在 5.1.37 版本之前不支持这些类型。版本不匹配导致了异常。
风暴来临:空指针异常
系统上线后没两天,新的问题出现了。当我们在订单表中插入一条 pay_time 为 NULL 的记录后,再次执行查询。

再次从堆栈入手,定位到 mysql-connector-java 的 JDBC42ResultSet.getObject 方法。

问题很明显:当从数据库获取的 Timestamp 为 NULL 时,调用 toLocalDateTime() 自然会抛出 NullPointerException。这是 mysql-connector-java 5.1.37 版本的一个缺陷。
查询该驱动的更新日志,发现在 5.1.42 版本中修复了此问题。

将驱动升级至 5.1.42 后,问题得以解决。


从一个事故看“修复”的边界
这让我想起亲身经历的一次生产事故。一个业务逻辑是校验附属文件依赖的主文件是否全部生成。优化前的校验代码存在逻辑错误,只要有一个主文件生成就校验通过,而业务要求是必须全部生成。
我“修复”了这个Bug,使其符合正确逻辑。然而上线后,一个多年未出问题的附属文件生成失败了。排查发现,数据库中关联字段里混入了脏数据(文件名与ID并存)。之前的错误逻辑阴差阳错地“兼容”了这条脏数据,而正确的逻辑反而使其暴露,导致流程中断。

优化前的错误逻辑

优化后的正确逻辑
这件事的教训是:在复杂的系统,尤其是遗留系统中,某些“Bug”可能已经与特定的数据状态或流程形成了脆弱的平衡。盲目的“修复”可能会破坏这种平衡,引发新的问题。
总结
无论是框架升级还是代码重构,都可能“牵一发而动全身”。在数据库驱动、ORM框架等基础组件的版本兼容性上,需要格外谨慎。MyBatis-Plus 与 MyBatis 以及 JDBC 驱动之间的版本耦合,就是典型的例子。
在进行任何改动前,充分的测试(包括边界情况、异常数据)至关重要。对于历史遗留的、运行稳定的代码,秉持“如无必要,勿增实体”的谨慎态度往往是明智的。如果不得不改,那么全面评估影响、准备回滚方案、进行灰度发布等工程实践必不可少。技术决策不应只关注代码的正确性,还需考虑系统的历史包袱和稳定性。在 云栈社区 中,我们经常讨论此类技术债务与工程实践的平衡之道。
