开心一刻
减肥是有技巧的,比如我朋友
早上一杯水,中午一个鸡蛋,晚上一碗稀粥
每天 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 COMM服务器托管网ENT = '订单'; 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 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
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的官方说明
既然基于Mybatis3.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 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()); } }
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,这不是文件名吗?
正常来讲应该是fileId,是一个自增的正整数呀,怎么会是文件名了?
脑中瞬间闪过一个念头:数据库数据有问题?
一查吓一跳,这个附属文件关联主文件的字段值是:4356,abc_{yyyyMMdd}.txt,看最终修改时间是:2021-08-21 15:22:12.652
4356文件的文件名就是abc_{yyyyMMdd}.txt,正常来讲,这个关联字段的值应该是:4356
敢情这个校验Bug完美的兼容了这个脏数据,所以几年了,一直没出现异常
是不是有这味了?
这可倒好,我把Bug修好,还出现问题了,你说我是不是手贱?
经此一役,我眼里的光又暗淡了些许
总结
关于对组件的升级,或者对旧代码的调整,都有可能牵一发动全身,影响甚大
我的观点是:能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑
如果到了不得不改的地步了,那就需要全面的测试
知乎上有个提问的回答百花齐放,很有意思,推荐给大家:为什么程序员会有代码能跑就不要动的观点?
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
相关推荐: 众筹系统开发丨DAPP智能合约泰山众筹系统开发功能逻辑及详细模板
什么是DAPP 根据David Johnston在文章DavidJohnstonCEO/Decentralized服务器托管网Applications里的定义,只有当满足以下所有条件时,一个应用才可以称之为DAPP。 应用必须完全开源、自治并且没有一个实体…