av一区二区在线观看_亚洲男人的天堂网站_日韩亚洲视频_在线成人免费_欧美日韩精品免费观看视频_久草视

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

瀏覽:18日期:2023-10-23 13:54:38

背景

最近有一個(gè)數(shù)據(jù)統(tǒng)計(jì)服務(wù)需要升級(jí) SpringBoot 的版本,由 1.5.x.RELEASE 直接升級(jí)到 2.3.0.RELEASE ,考慮到?jīng)]有用到 SpringBoot 的內(nèi)建 SPI ,升級(jí)過(guò)程算是順利。但是出于代碼潔癖和版本潔癖,看到項(xiàng)目中依賴(lài)的 MyBatis 的版本是 3.4.5 ,相比當(dāng)時(shí)的最新版本 3.5.5 大有落后,于是順便把它升級(jí)到 3.5.5 。升級(jí)完畢之后,執(zhí)行所有現(xiàn)存的集成測(cè)試,發(fā)現(xiàn)有部分 OffsetDateTime 類(lèi)型入?yún)⒌牟樵?xún)方法出現(xiàn)異常,于是進(jìn)行源碼層面的 DEBUG 找到最終的問(wèn)題并且解決。

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

問(wèn)題復(fù)現(xiàn)

項(xiàng)目中有一個(gè)查詢(xún)方法類(lèi)似下面的演示例子:

public interface OrderMapper { List<Order> selectByCreateTime(@Param('startCreateTime') OffsetDateTime startCreateTime, @Param('endCreateTime') OffsetDateTime endCreateTime);}

對(duì)應(yīng)的 XML 文件中的 SQL 代碼段如下:

<select resultMap='BaseResultMap'> SELECT * FROM t_order WHERE deleted = 0 AND create_time <![CDATA[>=]]> #{startCreateTime} AND create_time <![CDATA[<=]]> #{e ndCreateTime}</select>

上面的 OrderMapper#selectByCreateTime() 方法在 MyBatis 版本為 3.4.5 的前提下執(zhí)行沒(méi)有任何異常,當(dāng) MyBatis 版本升級(jí)為 3.5.5 后再次執(zhí)行,在 SQL 執(zhí)行日志輸出正確的前提下返回了一個(gè)空集合,具體的內(nèi)容如下:

查詢(xún)訂單列表:[]

雖然上帝視角是確認(rèn)了入?yún)⒔馕鲇袉?wèn)題,但是基于第一次發(fā)生異常的日志,其實(shí)定位不到具體發(fā)生問(wèn)題的位置,當(dāng)時(shí)條件反射認(rèn)為有幾處地方會(huì)出現(xiàn)這類(lèi)異常( SQL 比較簡(jiǎn)單,可以排除人為寫(xiě)錯(cuò) SQL 占位符的情況):

MyBatis 解析 OffsetDateTime 類(lèi)型方法參數(shù)的方法有版本兼容問(wèn)題。 MySQL 驅(qū)動(dòng)包解析 OffsetDateTime 類(lèi)型的參數(shù)有版本兼容問(wèn)題。 前面兩種情況混合相互影響導(dǎo)致的,其實(shí)這里也可以理解為同一種情況,因?yàn)?MyBatis 歸根到底是對(duì) MySQL 驅(qū)動(dòng)包進(jìn)行了封裝。

當(dāng)時(shí)項(xiàng)目中使用的 mysql-connector-java 版本為 8.0.18 ,并未升級(jí)為當(dāng)前的最新版本 8.0.21 ,所以當(dāng)時(shí)也有懷疑是低版本 MySQL 驅(qū)動(dòng)包沒(méi)有兼容解析 OffsetDateTime 類(lèi)型的參數(shù)。

簡(jiǎn)析MyBatis的執(zhí)行流程

MyBatis 的源碼并不復(fù)雜,如果省去分析它的配置和映射文件解析模塊,一個(gè)查詢(xún) SQL ( SelectList )的執(zhí)行流程大致如下:

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

當(dāng)然,因?yàn)閱?wèn)題出現(xiàn)在參數(shù)解析部分,只需要關(guān)注 StatementHandler 的處理邏輯即可。 StatementHandler 的父類(lèi) BaseStatementHandler 構(gòu)造函數(shù)中,初始化了 ParameterHandler 和 ResultSetHandler 實(shí)例,提交到 SimpleExecutor 中的 doQuery() 方法中執(zhí)行,使用了占位符參數(shù)的查詢(xún)會(huì)經(jīng)由 doQuery() 方法中的 prepareStatement() 方法然后調(diào)用 PreparedStatementHandler#parameterize() ,最終委托到 DefaultParameterHandler#setParameters() 方法進(jìn)行參數(shù)設(shè)置,這個(gè) setParameters() 方法會(huì)用到 ParameterMapping 和 TypeHandler 。

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

如果用到了內(nèi)建的 TypeHandler 或者自定義的 TypeHandler 實(shí)現(xiàn),同時(shí)出現(xiàn)了參數(shù)解析異常,那么很大幾率異常就是從 DefaultParameterHandler#setParameters() 方法中出現(xiàn),這樣就能順藤摸瓜找到出現(xiàn)異常的 TypeHandler 。

參數(shù)解析異常的根本原因

本文前面提到的解析 OffsetDateTime 類(lèi)型異常,實(shí)際上執(zhí)行查詢(xún)的時(shí)候代碼會(huì)步入 OffsetDateTimeTypeHandler ,這里對(duì)比一下 3.4.5 和 3.5.5 版本中 MyBatis 對(duì)應(yīng)的 OffsetDateTimeTypeHandler 實(shí)現(xiàn):

發(fā)現(xiàn)了主要區(qū)別如下:

3.4.5 版本中,會(huì)把 OffsetDateTime 參數(shù)類(lèi)型轉(zhuǎn)換為 Timestamp 類(lèi)型,再委托到 PreparedStatement#setTimestamp() 進(jìn)行參數(shù)設(shè)置。

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

3.5.5 版本中,直接調(diào)用 PreparedStatement#setObject() 進(jìn)行參數(shù)設(shè)置。

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

PreparedStatement#setTimestamp() 是很早期的產(chǎn)物,這個(gè)方法是沒(méi)有任何問(wèn)題的, 3.4.5 版本 MyBatis 把 OffsetDateTime 類(lèi)型兼容為 Timestamp 類(lèi)型處理 。那么基本可以確定問(wèn)題出現(xiàn)在 PreparedStatement#setObject() 方法上,對(duì)于 MySQL8.x 的驅(qū)動(dòng), PreparedStatement 選用的實(shí)現(xiàn)類(lèi)是 com.mysql.cj.jdbc.ClientPreparedStatement ,通過(guò)層層 DEBUG 最終到達(dá) AbstractQueryBindings#setObject() 方法:

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

由于驅(qū)動(dòng)中沒(méi)有任何解析 OffsetDateTime 類(lèi)型的片段,所以最終會(huì)使用 AbstractQueryBindings#setSerializableObject() 方法(也就是 else 分支的代碼)兜底,直接轉(zhuǎn)化為一個(gè) byte[] 傳輸?shù)?MySQL 服務(wù)端, 問(wèn)題就出在這里,直接把 OffsetDateTime 類(lèi)型序列化疑似在 MySQL 服務(wù)端拿到的不是預(yù)期的參數(shù),導(dǎo)致查詢(xún)條件出現(xiàn)失效(這里筆者沒(méi)有花時(shí)間去閱讀 MySQL 的協(xié)議,也沒(méi)有花大量時(shí)間去抓包,所以這里還只是猜測(cè)) 。然而, 這個(gè)問(wèn)題在 2020-7-12 最新發(fā)布的 mysql:mysql-connector-java:8.0.21 依然沒(méi)有解決 。但是看到這里又出現(xiàn)一個(gè)疑惑, MyBatis 的開(kāi)發(fā)者應(yīng)該不可能在這種關(guān)鍵而不復(fù)雜的問(wèn)題上出現(xiàn)紕漏,于是花時(shí)間去看看這里的代碼提交記錄:

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

這是 Raupach 在 2017-08-22 的一個(gè)提交,提交的 message 是:測(cè)試 OffsetDateTimeHandler 保留了 UTC 的偏移量。單元測(cè)試類(lèi) OffsetDateTimeTypeHandlerTest 也只是驗(yàn)證了 TypeHandler#setParameter() 和 PreparedStatement#setObject() 參數(shù)傳遞的正確性, 并沒(méi)有做集成測(cè)試去跟蹤所有類(lèi)型數(shù)據(jù)庫(kù)的傳參問(wèn)題,估計(jì)就是這一步疏忽了,但是這個(gè)應(yīng)該不屬于MyBatis的問(wèn)題,畢竟它只是對(duì)數(shù)據(jù)庫(kù)驅(qū)動(dòng)包的封裝 。其中集成測(cè)試 TimestampWithTimezoneTypeHandlerTest 使用了內(nèi)存數(shù)據(jù)庫(kù),這里可以猜測(cè)是 HSQLDB 驅(qū)動(dòng)完善了日期時(shí)間的參數(shù)解析。

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

同樣的問(wèn)題在 h2 數(shù)據(jù)庫(kù)中不會(huì)出現(xiàn),于是稍微 DEBUG 了一下 h2 數(shù)據(jù)庫(kù)驅(qū)動(dòng)進(jìn)行參數(shù)設(shè)置的源碼,最終定位到 org.h2.value.DataType (驅(qū)動(dòng)包的版本為 com.h2database:h2:1.4.200 )的第 1333 行有對(duì)應(yīng) JSR310.OFFSET_DATE_TIME 的解析邏輯,所以 h2 數(shù)據(jù)庫(kù)驅(qū)動(dòng)可以支持所有 JSR310 引入的參數(shù)類(lèi)型的參數(shù)值設(shè)置。下面的截圖是 h2 數(shù)據(jù)庫(kù)驅(qū)動(dòng)中 PreparedStatement#setObject() 的解析實(shí)現(xiàn)(見(jiàn) org.h2.jdbc.JdbcPreparedStatement 和 DataType#convertToValue() 的源碼):

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

這里可見(jiàn), h2 的驅(qū)動(dòng)真的對(duì) JDK8+ 新增的所有日期時(shí)間類(lèi)型都做了解析:

MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)

針對(duì)問(wèn)題的解決方案

如果選用了 MySQL ,這個(gè)參數(shù)解析異常的問(wèn)題截至 mysql:mysql-connector-java:8.0.21 只有一種解決方案:要把 OffsetDateTime 類(lèi)型兼容為 Timestamp 類(lèi)型進(jìn)行參數(shù)設(shè)置。其實(shí)對(duì)于所有非 LocalXX 的日期時(shí)間類(lèi)型都需要進(jìn)行兼容,兼容表格如下:

序號(hào) 類(lèi)型 兼容類(lèi)型 調(diào)用方法 1 OffsetDateTime Timestamp PreparedStatement#setTimestamp() 2 ZonedDateTime Timestamp PreparedStatement#setTimestamp() 3 OffsetDate java.sql.Date PreparedStatement#setDate() 4 OffsetTime java.sql.Time PreparedStatement#setTime()

以 OffsetDateTime 為例,只需要參考或者直接使用 3.4.5 版本中的 MyBatis 的 OffsetDateTimeTypeHandler ,然后通過(guò)配置直接覆蓋內(nèi)置實(shí)現(xiàn)即可。

// 假設(shè)全類(lèi)名為club.throwable.OffsetDateTimeTypeHandlerpublic class OffsetDateTimeTypeHandler extends BaseTypeHandler<OffsetDateTime> { @Override public void setNonNullParameter(PreparedStatement ps, int i, OffsetDateTime parameter, JdbcType jdbcType) throws SQLException { ps.setTimestamp(i, Timestamp.from(parameter.toInstant())); } @Override public OffsetDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException { Timestamp timestamp = rs.getTimestamp(columnName); return getOffsetDateTime(timestamp); } @Override public OffsetDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException { Timestamp timestamp = rs.getTimestamp(columnIndex); return getOffsetDateTime(timestamp); } @Override public OffsetDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { Timestamp timestamp = cs.getTimestamp(columnIndex); return getOffsetDateTime(timestamp); } private static OffsetDateTime getOffsetDateTime(Timestamp timestamp) { if (timestamp != null) { // 這里可以考慮自定義系統(tǒng)的時(shí)區(qū),例如ZoneId.of('Asia/Shanghai') return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault()); } return null; }}

配置文件中進(jìn)行 TypeHandler 配置覆蓋,下面是類(lèi)路徑下配置文件 mybatis-config.xml 的示例:

<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE configuration PUBLIC '-//mybatis.org//DTD Config 3.0//EN' 'http://mybatis.org/dtd/mybatis-3-config.dtd'><configuration> <settings> <!--下劃線(xiàn)轉(zhuǎn)駝峰--> <setting name='mapUnderscoreToCamelCase' value='true'/> <!--未知列映射忽略--> <setting name='autoMappingUnknownColumnBehavior' value='NONE'/> </settings> <typeHandlers> <!--覆蓋內(nèi)置OffsetDateTimeTypeHandler--> <typeHandler handler='throwable.club.OffsetDateTimeTypeHandler'/> </typeHandlers></configuration>

其他類(lèi)型解析異常都可以參照此思路進(jìn)行兼容。

小結(jié)

升級(jí)基礎(chǔ)框架版本需要謹(jǐn)慎。另外,文中提到的解決方案只是筆者目前通過(guò)問(wèn)題分析和定位得到的一種相對(duì)合理的解決方案,也可能有更優(yōu)解。

本文的 demo 項(xiàng)目倉(cāng)庫(kù):

Github : https://github.com/zjcscut/spring-boot-guide/tree/master/ch9-mybatis-mysql

到此這篇關(guān)于MyBatis版本升級(jí)導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓?wèn)題復(fù)盤(pán)的文章就介紹到這了,更多相關(guān)MyBatis OffsetDateTime入?yún)惓?nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!

相關(guān)文章:
主站蜘蛛池模板: 91免费在线视频 | 一区二区三区四区av | 精品欧美一区二区在线观看欧美熟 | 精品国产一区二区在线 | 欧美日韩在线综合 | 日本一二区视频 | 一区二区三区精品视频 | 日韩高清三区 | 成人性视频免费网站 | 欧美视频一区二区三区 | 成人福利在线观看 | 日日骚av | 欧美日韩三区 | 国产激情一区二区三区 | 成人高清视频在线观看 | 欧美专区日韩专区 | 国产一区免费 | 久久久青草婷婷精品综合日韩 | 国产精品一区在线播放 | 天天插天天操 | 久久久久久久久久久丰满 | 91免费在线 | 国产精品美女久久久久久免费 | 中文字幕在线剧情 | 久久免费资源 | 涩涩视频在线观看免费 | 日韩在线欧美 | 国产精品一区二区在线 | 久久久久久成人 | 99re视频精品 | 瑟瑟免费视频 | 免费观看一级毛片 | 国产精品久久久久久久久免费高清 | 羞羞视频免费观看入口 | 欧美在线视频免费 | 在线观看www视频 | 精品一区二区久久久久久久网站 | 日韩一区二区三区在线观看 | 亚洲视频三| 乳色吐息在线观看 | 国产成人午夜精品影院游乐网 |