當前位置: 妍妍網 > 碼農

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

2024-02-21碼農

推薦關註

掃碼關註 後端架構師 」,選擇 星標 公眾號

重磅幹貨,第一時間送達!

責編:架構君 | 來源:cnblogs.com/youzhibing/p/18019399

上一篇好文:

正文

大家好,我是後端架構師。

背景介紹

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

為了簡化演示,我就直接用 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 fortype java.time.LocalDateTime
; Conversion not supported fortype java.time.LocalDateTime; nested exception is java.sql.SQLException: Conversion not supported fortype 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 fortype 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

我們再回過頭去看看前面說到的異常: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);
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);
// } elseif (type.equals(Struct. class)) {
//
// }
// } elseif (type.equals(RowId. class)) {
//
// } elseif (type.equals(NClob. class)) {
//
// } elseif (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 修好,還出現問題了,你說我是不是手賤?

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

總結

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

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

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

你還有什麽想要補充的嗎?

最後,再次推薦下我們的AI星

為了跟上AI時代我幹了一件事兒,我建立了一個知識星球社群:ChartGPT與副業。想帶著大家一起探索 ChatGPT和新的AI時代

有很多小夥伴搞不定ChatGPT帳號,於是我們決定,凡是這三天之內加入ChatPGT的小夥伴,我們直接送一個正常可用的永久ChatGPT獨立帳戶。

不光是增長速度最快,我們的星球品質也絕對經得起考驗,短短一個月時間,我們的課程團隊釋出了 8個專欄、18個副業計畫

簡單說下這個星球能給大家提供什麽:

1、不斷分享如何使用ChatGPT來完成各種任務,讓你更高效地使用ChatGPT,以及副業思考、變現思路、創業案例、落地案例分享。

2、分享ChatGPT的使用方法、最新資訊、商業價值。

3、探討未來關於ChatGPT的機遇,共同成長。

4、幫助大家解決ChatGPT遇到的問題。

5、 提供一整年的售後服務,一起搞副業

星球福利:

1、加入星球4天後,就送ChatGPT獨立帳號。

2、邀請你加入ChatGPT會員交流群。

3、贈送一份完整的ChatGPT手冊和66個ChatGPT副業賺錢手冊。

其它福利還在籌劃中... 不過,我給你大家保證,加入星球後,收獲的價值會遠遠大於今天加入的門票費用 !

本星球第一期原價 399 ,目前屬於試營運,早鳥價 139 ,每超過50人漲價10元,星球馬上要來一波大的漲價,如果你還在猶豫,可能最後就要以 更高價格加入了 。。

早就是優勢。 建議大家盡早以便宜的價格加入!

歡迎有需要的同學試試,如果本文對您有幫助,也請幫忙點個 贊 + 在看 啦!❤️

在 還有更多優質計畫系統學習資源,歡迎分享給其他同學吧!

PS:如果覺得我的分享不錯,歡迎大家隨手點贊、轉發、在看。

最後給讀者整理了一份BAT大廠面試真題,需要的可掃碼加微信備註:「面試」獲取。

版權申明:內容來源網路,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

END

最近面試BAT,整理一份面試資料【Java面試BAT通關手冊】,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。在這裏,我為大家準備了一份2021年最新最全BAT等大廠Java面試經驗總結。

別找了,想獲取史上最全的Java大廠面試題學習資料

掃下方二維碼回復面試就好了

歷史好文:

掃碼關註後端架構師」,選擇星標公眾號

重磅幹貨,第一時間送達

,你在看嗎?