MyBatis版本升級導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤
背景
最近有一個數(shù)據(jù)統(tǒng)計服務(wù)需要升級 SpringBoot
的版本,由 1.5.x.RELEASE
直接升級到 2.3.0.RELEASE
,考慮到?jīng)]有用到 SpringBoot
的內(nèi)建 SPI
,升級過程算是順利。但是出于代碼潔癖和版本潔癖,看到項目中依賴的 MyBatis
的版本是 3.4.5
,相比當(dāng)時的最新版本 3.5.5
大有落后,于是順便把它升級到 3.5.5
。升級完畢之后,執(zhí)行所有現(xiàn)存的集成測試,發(fā)現(xiàn)有部分 OffsetDateTime
類型入?yún)⒌牟樵兎椒ǔ霈F(xiàn)異常,于是進(jìn)行源碼層面的 DEBUG
找到最終的問題并且解決。
問題復(fù)現(xiàn)
項目中有一個查詢方法類似下面的演示例子:
public interface OrderMapper { List<Order> selectByCreateTime(@Param("startCreateTime") OffsetDateTime startCreateTime, @Param("endCreateTime") OffsetDateTime endCreateTime); }
對應(yīng)的 XML
文件中的 SQL
代碼段如下:
<select id="selectByCreateTime" 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í)行沒有任何異常,當(dāng) MyBatis
版本升級為 3.5.5
后再次執(zhí)行,在 SQL
執(zhí)行日志輸出正確的前提下返回了一個空集合,具體的內(nèi)容如下:
查詢訂單列表:[]
雖然上帝視角是確認(rèn)了入?yún)⒔馕鲇袉栴},但是基于第一次發(fā)生異常的日志,其實定位不到具體發(fā)生問題的位置,當(dāng)時條件反射認(rèn)為有幾處地方會出現(xiàn)這類異常( SQL
比較簡單,可以排除人為寫錯 SQL
占位符的情況):
MyBatis
解析OffsetDateTime
類型方法參數(shù)的方法有版本兼容問題。MySQL
驅(qū)動包解析OffsetDateTime
類型的參數(shù)有版本兼容問題。- 前面兩種情況混合相互影響導(dǎo)致的,其實這里也可以理解為同一種情況,因為
MyBatis
歸根到底是對MySQL
驅(qū)動包進(jìn)行了封裝。
當(dāng)時項目中使用的 mysql-connector-java
版本為 8.0.18
,并未升級為當(dāng)前的最新版本 8.0.21
,所以當(dāng)時也有懷疑是低版本 MySQL
驅(qū)動包沒有兼容解析 OffsetDateTime
類型的參數(shù)。
簡析MyBatis的執(zhí)行流程
MyBatis
的源碼并不復(fù)雜,如果省去分析它的配置和映射文件解析模塊,一個查詢 SQL
( SelectList
)的執(zhí)行流程大致如下:
當(dāng)然,因為問題出現(xiàn)在參數(shù)解析部分,只需要關(guān)注 StatementHandler
的處理邏輯即可。 StatementHandler
的父類 BaseStatementHandler
構(gòu)造函數(shù)中,初始化了 ParameterHandler
和 ResultSetHandler
實例,提交到 SimpleExecutor
中的 doQuery()
方法中執(zhí)行,使用了占位符參數(shù)的查詢會經(jīng)由 doQuery()
方法中的 prepareStatement()
方法然后調(diào)用 PreparedStatementHandler#parameterize()
,最終委托到 DefaultParameterHandler#setParameters()
方法進(jìn)行參數(shù)設(shè)置,這個 setParameters()
方法會用到 ParameterMapping
和 TypeHandler
。
如果用到了內(nèi)建的 TypeHandler
或者自定義的 TypeHandler
實現(xiàn),同時出現(xiàn)了參數(shù)解析異常,那么很大幾率異常就是從 DefaultParameterHandler#setParameters()
方法中出現(xiàn),這樣就能順藤摸瓜找到出現(xiàn)異常的 TypeHandler
。
參數(shù)解析異常的根本原因
本文前面提到的解析 OffsetDateTime
類型異常,實際上執(zhí)行查詢的時候代碼會步入 OffsetDateTimeTypeHandler
,這里對比一下 3.4.5
和 3.5.5
版本中 MyBatis
對應(yīng)的 OffsetDateTimeTypeHandler
實現(xiàn):
發(fā)現(xiàn)了主要區(qū)別如下:
3.4.5
版本中,會把 OffsetDateTime
參數(shù)類型轉(zhuǎn)換為 Timestamp
類型,再委托到 PreparedStatement#setTimestamp()
進(jìn)行參數(shù)設(shè)置。
3.5.5
版本中,直接調(diào)用 PreparedStatement#setObject()
進(jìn)行參數(shù)設(shè)置。
PreparedStatement#setTimestamp()
是很早期的產(chǎn)物,這個方法是沒有任何問題的, 3.4.5
版本 MyBatis
把 OffsetDateTime
類型兼容為 Timestamp
類型處理 。那么基本可以確定問題出現(xiàn)在 PreparedStatement#setObject()
方法上,對于 MySQL8.x
的驅(qū)動, PreparedStatement
選用的實現(xiàn)類是 com.mysql.cj.jdbc.ClientPreparedStatement
,通過層層 DEBUG
最終到達(dá) AbstractQueryBindings#setObject()
方法:
由于驅(qū)動中沒有任何解析 OffsetDateTime
類型的片段,所以最終會使用 AbstractQueryBindings#setSerializableObject()
方法(也就是 else
分支的代碼)兜底,直接轉(zhuǎn)化為一個 byte[]
傳輸?shù)?MySQL
服務(wù)端, 問題就出在這里,直接把 OffsetDateTime
類型序列化疑似在 MySQL
服務(wù)端拿到的不是預(yù)期的參數(shù),導(dǎo)致查詢條件出現(xiàn)失效(這里筆者沒有花時間去閱讀 MySQL
的協(xié)議,也沒有花大量時間去抓包,所以這里還只是猜測) 。然而, 這個問題在 2020-7-12
最新發(fā)布的 mysql:mysql-connector-java:8.0.21
依然沒有解決 。但是看到這里又出現(xiàn)一個疑惑, MyBatis
的開發(fā)者應(yīng)該不可能在這種關(guān)鍵而不復(fù)雜的問題上出現(xiàn)紕漏,于是花時間去看看這里的代碼提交記錄:
這是 Raupach
在 2017-08-22
的一個提交,提交的 message
是:測試 OffsetDateTimeHandler
保留了 UTC
的偏移量。單元測試類 OffsetDateTimeTypeHandlerTest
也只是驗證了 TypeHandler#setParameter()
和 PreparedStatement#setObject()
參數(shù)傳遞的正確性, 并沒有做集成測試去跟蹤所有類型數(shù)據(jù)庫的傳參問題,估計就是這一步疏忽了,但是這個應(yīng)該不屬于MyBatis的問題,畢竟它只是對數(shù)據(jù)庫驅(qū)動包的封裝 。其中集成測試 TimestampWithTimezoneTypeHandlerTest
使用了內(nèi)存數(shù)據(jù)庫,這里可以猜測是 HSQLDB
驅(qū)動完善了日期時間的參數(shù)解析。
同樣的問題在 h2
數(shù)據(jù)庫中不會出現(xiàn),于是稍微 DEBUG
了一下 h2
數(shù)據(jù)庫驅(qū)動進(jìn)行參數(shù)設(shè)置的源碼,最終定位到 org.h2.value.DataType
(驅(qū)動包的版本為 com.h2database:h2:1.4.200
)的第 1333
行有對應(yīng) JSR310.OFFSET_DATE_TIME
的解析邏輯,所以 h2
數(shù)據(jù)庫驅(qū)動可以支持所有 JSR310
引入的參數(shù)類型的參數(shù)值設(shè)置。下面的截圖是 h2
數(shù)據(jù)庫驅(qū)動中 PreparedStatement#setObject()
的解析實現(xiàn)(見 org.h2.jdbc.JdbcPreparedStatement
和 DataType#convertToValue()
的源碼):
這里可見, h2
的驅(qū)動真的對 JDK8+
新增的所有日期時間類型都做了解析:
針對問題的解決方案
如果選用了 MySQL
,這個參數(shù)解析異常的問題截至 mysql:mysql-connector-java:8.0.21
只有一種解決方案:要把 OffsetDateTime
類型兼容為 Timestamp
類型進(jìn)行參數(shù)設(shè)置。其實對于所有非 LocalXX
的日期時間類型都需要進(jìn)行兼容,兼容表格如下:
序號 | 類型 | 兼容類型 | 調(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
,然后通過配置直接覆蓋內(nèi)置實現(xiàn)即可。
// 假設(shè)全類名為club.throwable.OffsetDateTimeTypeHandler public 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)的時區(qū),例如ZoneId.of("Asia/Shanghai") return OffsetDateTime.ofInstant(timestamp.toInstant(), ZoneId.systemDefault()); } return null; } }
配置文件中進(jìn)行 TypeHandler
配置覆蓋,下面是類路徑下配置文件 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> <!--下劃線轉(zhuǎn)駝峰--> <setting name="mapUnderscoreToCamelCase" value="true"/> <!--未知列映射忽略--> <setting name="autoMappingUnknownColumnBehavior" value="NONE"/> </settings> <typeHandlers> <!--覆蓋內(nèi)置OffsetDateTimeTypeHandler--> <typeHandler handler="throwable.club.OffsetDateTimeTypeHandler"/> </typeHandlers> </configuration>
其他類型解析異常都可以參照此思路進(jìn)行兼容。
小結(jié)
升級基礎(chǔ)框架版本需要謹(jǐn)慎。另外,文中提到的解決方案只是筆者目前通過問題分析和定位得到的一種相對合理的解決方案,也可能有更優(yōu)解。
本文的 demo
項目倉庫:
Github
: https://github.com/zjcscut/spring-boot-guide/tree/master/ch9-mybatis-mysql
到此這篇關(guān)于MyBatis版本升級導(dǎo)致OffsetDateTime入?yún)⒔馕霎惓栴}復(fù)盤的文章就介紹到這了,更多相關(guān)MyBatis OffsetDateTime入?yún)惓?nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- MyBatis異常-Property ''configLocation'' not specified, using default MyBatis Configuration
- Mybatis單個參數(shù)的if判斷報異常There is no getter for property named ''xxx'' in ''class java.lang.Integer''的解決方案
- mybatis報錯元素內(nèi)容必須由格式正確的字符數(shù)據(jù)或標(biāo)記組成異常的解決辦法
- 使用Mybatis遇到的there is no getter異常
- Mybatis foreach標(biāo)簽使用不當(dāng)導(dǎo)致異常的原因淺析
相關(guān)文章
datatables 帶查詢條件java服務(wù)端分頁處理實例
本篇文章主要介紹了datatables 帶查詢條件java服務(wù)端分頁處理實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06Java實戰(zhàn)權(quán)限管理系統(tǒng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SpringBoot+MyBatis+AOP+LayUI+Mysql實現(xiàn)一個權(quán)限管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2022-01-01SpringBoot使用Jwt處理跨域認(rèn)證問題的教程詳解
這篇文章主要介紹了SpringBoot使用Jwt處理跨域認(rèn)證問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06SpringBoot內(nèi)嵌tomcat處理有特殊字符轉(zhuǎn)義的問題
這篇文章主要介紹了SpringBoot內(nèi)嵌tomcat處理有特殊字符轉(zhuǎn)義的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06Spring Boot不同版本Redis設(shè)置JedisConnectionFactory詳解
本文章向大家介紹Spring Boot不同版本Redis設(shè)置JedisConnectionFactory,主要內(nèi)容包括1.X 版本、2.X 版本、2.、基本概念、基礎(chǔ)應(yīng)用、原理機(jī)制和需要注意的事項等,并結(jié)合實例形式分析了其使用技巧,希望通過本文能幫助到大家理解應(yīng)用這部分內(nèi)容2023-09-09java 實現(xiàn)當(dāng)前時間加減30分鐘的時間代碼
這篇文章主要介紹了java 實現(xiàn)當(dāng)前時間加減30分鐘的時間代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08