开心一刻

减肥是有技巧的,比如我朋友

早上一杯水,中午一个鸡蛋,晚上一碗稀粥

每天 10 公里,500 个跳绳,200 个蛙跳

他以前 180 斤,现在连人带盒才 5 斤

就问你们,这减肥效果是不是杠杠的?

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

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
,会报错,异常信息如下


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
for type 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.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
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

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
的问题?

如何找这个异常的根因了?

很简单,直接从异常堆栈入手

点了之后,你会发现方法很简单

这么简单的代码能有什么问题?

大家注意看图中左上角
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) throwsSQLException {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);
throwsqlEx;
}
}
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.666666', '2024-02-21 20:02:56.764');

再执行
com.qsl.OrderTest#orderListAllTest

此刻我就想问
"小伙"
:刺不刺激?

碰到了异常,那就找原因

同样从异常堆栈入手

看出什么了?

如果
getTimestamp(columnIndex)
得到的是
NULL
,不就
NullPointerException
? 严谨性了?

修复问题要紧,我们先看哪个版本进行修复了?


mysql-connector-java
升级到
5.1.42

问题得以修复

经此一役,
"小伙"
似乎成长了很多,但眼里的光却暗淡了不少

mybatis-plus-issues-6666664

无意中看到了这个
issue-6666664
,跟我们前面分析的
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
修好,还出现问题了,你说我是不是手贱?

经此一役,我眼里的光又暗淡了些许

总结

关于对组件的升级,或者对旧代码的调整,都有可能牵一发动全身,影响甚大

我的观点是:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑

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

知乎上有个提问的回答百花齐放,很有意思,推荐给大家:
为什么程序员会有代码能跑就不要动的观点?

标签: none

添加新评论