當前位置: 妍妍網 > 碼農

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

2024-02-21碼農

大家好,我是鵬磊。

一個老計畫,資料庫用的是 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');

View Code

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

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

Mybatis-Plus 版本就用 "小夥" 參照的版本: 3.1 . 1 , mysql-connector-java 版本

保持不變還是 5.1 . 26

範例程式碼:play_it_safe

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

如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。

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.SpringJUnit4 classRunner.runChild(SpringJUnit4 classRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4 classRunner.runChild(SpringJUnit4 classRunner.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.RunBeforeTest classCallbacks.evaluate(RunBeforeTest classCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTest classCallbacks.evaluate(RunAfterTest classCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4 classRunner.run(SpringJUnit4 classRunner.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

View Code

註意看 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 的問題?

如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。

如何找這個異常的根因了?

很簡單,直接從異常堆疊入手

點了之後,你會發現方法很簡單

這麽簡單的程式碼能有什麽問題?

大家註意看圖中左上角 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> 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);
elseif (type.equals(BigDecimal. class)) {
return (T) getBigDecimal(columnIndex);
elseif (type.equals(Boolean. class) || type.equals(Boolean.TYPE)) {
return (T) Boolean.valueOf(getBoolean(columnIndex));
elseif (type.equals(Integer. class) || type.equals(Integer.TYPE)) {
return (T) Integer.valueOf(getInt(columnIndex));
elseif (type.equals(Long. class) || type.equals(Long.TYPE)) {
return (T) Long.valueOf(getLong(columnIndex));
elseif (type.equals(Float. class) || type.equals(Float.TYPE)) {
return (T) Float.valueOf(getFloat(columnIndex));
elseif (type.equals(Double. class) || type.equals(Double.TYPE)) {
return (T) Double.valueOf(getDouble(columnIndex));
elseif (type.equals(byte[]. class)) {
return (T) getBytes(columnIndex);
elseif (type.equals(java.sql.Date. class)) {
return (T) getDate(columnIndex);
elseif (type.equals(Time. class)) {
return (T) getTime(columnIndex);
elseif (type.equals(Timestamp. class)) {
return (T) getTimestamp(columnIndex);
elseif (type.equals(Clob. class)) {
return (T) getClob(columnIndex);
elseif (type.equals(Blob. class)) {
return (T) getBlob(columnIndex);
elseif (type.equals(Array. class)) {
return (T) getArray(columnIndex);
elseif (type.equals(Ref. class)) {
return (T) getRef(columnIndex);
elseif (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());
}
}


View Code

確實沒有 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 ,這不是檔名嗎?

如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。

正常來講應該是 fileId ,是一個自增的正整數呀,怎麽會是檔名了?

腦中瞬間閃過一個念頭:資料庫數據有問題?

一查嚇一跳,這個附屬檔關聯主檔的欄位值是:4356,abc_{yyyyMMdd}.txt ,看最終修改時間是:2021-08-21 15:22:12.652

4356 檔的檔名就是 abc_{yyyyMMdd}.txt ,正常來講,這個關聯欄位的值應該是:4356

敢情這個 校驗Bug 完美的相容了這個臟數據 ,所以幾年了,一直沒出現異常

是不是有這味了?

這可倒好,我把 Bug 修好,還出現問題了,你說我是不是手賤?

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

總結

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

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

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

🔥 磊哥私藏精品 熱門推薦 🔥