公司新來一個幹練小夥,把 MyBatis 替換成 MyBatis-Plus,上線後哭暈在廁所。。。

Java技术栈發表於2024-04-22

作者:青石路
來源:https://www.cnblogs.com/youzhibing/p/18019399

MyBatis 替換成 MyBatis-Plus

背景介紹

一個老專案,資料庫用的是 MySQL 5.7.36 , ORM 框架用的 MyBatis 3.5.0 , mysql-connector-java 版本是 5.1.26

新來了一個幹練的小夥,精力充沛,看著就是一個喜歡折騰的主

他就覺得 MyBatis 使用起來不夠簡單,要寫的程式碼還比較多,覺得有必要替換成 MyBatis-Plus

Mybatis-Plus 替換 Mybatis

先準備一張表 tbl_order ,然後初始化 2 條資料

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');

推薦一個開源免費的 Spring Boot 實戰專案:

https://github.com/javastacks/spring-boot-best-practice

為了簡化演示,我就直接用 Mybatis-Plus 搭建一個示例 demo ,以此來模擬下 "小夥" 替換的過程

只是用 MyBatis-Plus 替換 MyBatis ,其他元件的版本暫不動

Mybatis-Plus 版本就用 "小夥" 引用的版本:3.1.1 , mysql-connector-java 版本保持不變還是 5.1.26

示例程式碼:play_it_safe

此時執行 com.qsl.OrderTest#orderListAllTest ,會報錯,異常資訊如下

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
; Conversion not supported for type java.time.LocalDateTime; nested exception is java.sql.SQLException: Conversion not supported for type java.time.LocalDateTime

    at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:110)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
    at com.sun.proxy.$Proxy53.selectList(Unknown Source)
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)
    at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:158)
    at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:76)
    at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:62)
    at com.sun.proxy.$Proxy59.selectList(Unknown Source)
    at com.qsl.OrderTest.orderListAllTest(OrderTest.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
    at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:232)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:55)
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.SQLError.createSQLException(SQLError.java:975)
    at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:920)
    at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5126)
    at com.mysql.jdbc.JDBC4ResultSet.getObject(JDBC4ResultSet.java:547)
    at com.mysql.jdbc.ResultSetImpl.getObject(ResultSetImpl.java:5133)
    at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.apache.ibatis.logging.jdbc.ResultSetLogger.invoke(ResultSetLogger.java:69)
    at com.sun.proxy.$Proxy71.getObject(Unknown Source)
    at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:38)
    at org.apache.ibatis.type.LocalDateTimeTypeHandler.getNullableResult(LocalDateTimeTypeHandler.java:28)
    at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:521)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:402)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
    at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
    at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:67)
    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
    ... 39 more

注意看 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 之前沒這個異常,替換之後就有了這個異常,這不是 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

那它支援哪些型別了?

我們同樣從異常堆疊入手

點了之後,可以看到下圖

public <T> T getObject(int columnIndex, Class<T> type) throws SQLException {
    if (type == null) {
        throw SQLError.createSQLException("Type parameter can not be null",
                SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
    }

    if (type.equals(String.class)) {
        return (T) getString(columnIndex);
    } else if (type.equals(BigDecimal.class)) {
        return (T) getBigDecimal(columnIndex);
    } else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) {
        return (T) Boolean.valueOf(getBoolean(columnIndex));
    } else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {
        return (T) Integer.valueOf(getInt(columnIndex));
    } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
        return (T) Long.valueOf(getLong(columnIndex));
    } else if (type.equals(Float.class) || type.equals(Float.TYPE)) {
        return (T) Float.valueOf(getFloat(columnIndex));
    } else if (type.equals(Double.class) || type.equals(Double.TYPE)) {
        return (T) Double.valueOf(getDouble(columnIndex));
    } else if (type.equals(byte[].class)) {
        return (T) getBytes(columnIndex);
    } else if (type.equals(java.sql.Date.class)) {
        return (T) getDate(columnIndex);
    } else if (type.equals(Time.class)) {
        return (T) getTime(columnIndex);
    } else if (type.equals(Timestamp.class)) {
        return (T) getTimestamp(columnIndex);
    } else if (type.equals(Clob.class)) {
        return (T) getClob(columnIndex);
    } else if (type.equals(Blob.class)) {
        return (T) getBlob(columnIndex);
    } else if (type.equals(Array.class)) {
        return (T) getArray(columnIndex);
    } else if (type.equals(Ref.class)) {
        return (T) getRef(columnIndex);
    } else if (type.equals(URL.class)) {
        return (T) getURL(columnIndex);
//        } else if (type.equals(Struct.class)) {
//
//            }
//        } else if (type.equals(RowId.class)) {
//
//        } else if (type.equals(NClob.class)) {
//
//        } else if (type.equals(SQLXML.class)) {

    } else {
        if (this.connection.getAutoDeserialize()) {
            try {
                return (T) getObject(columnIndex);
            } catch (ClassCastException cce) {
                SQLException sqlEx = SQLError.createSQLException("Conversion not supported for type " + type.getName(),
                        SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
                sqlEx.initCause(cce);

                throw sqlEx;
            }
        }

        throw SQLError.createSQLException("Conversion not supported for type " + type.getName(),
                SQLError.SQL_STATE_ILLEGAL_ARGUMENT, getExceptionInterceptor());
    }
}

確實沒有 LocalDateTime 、 LocalDate 和 LocalTime

mysql-connector-java 5.1.37 開始支援 LocalDateTime 、 LocalDate 和 LocalTime ,前面已經介紹過了,不再過多贅述

總結下異常根因:MyBatis 3.5.1 開始不再處理 LocalDateTime 、 LocalDate 和 LocalTime 的轉換,而 mysql-connector-java 5.1.37 之前都不支援這些型別

弄清楚這個異常的來龍去脈之後,順的是不是又理所當然一些了?

暴風雨的來臨

版本上線沒 2 天,該來的終究還是來了

我們往表 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 修好,還出現問題了,你說我是不是手賤?

經此一役,我眼裡的光又暗淡了些許

總結

關於對元件的升級,或者對舊程式碼的調整,都有可能牽一髮動全身,影響甚大

我的觀點是:能不動就不要動,改好沒績效,改出問題要背鍋,吃力不討好,又不是不能跑

如果到了不得不改的地步了,那就需要全面的測試。

更多文章推薦:

1.Spring Boot 3.x 教程,太全了!

2.2,000+ 道 Java面試題及答案整理(2024最新版)

3.免費獲取 IDEA 啟用碼的 7 種方式(2024最新版)

覺得不錯,別忘了隨手點贊+轉發哦!

相關文章