開心一刻
今天去超市買飲料
老闆說 5 元,我聽成了“會員”
我說沒有
老闆跟我對視了 10 秒
然後,老闆說:沒有你買啥?
我說:沒有就不讓買?
當時的老闆:
瑕疵回顧
都說了能不動就別動,非要去調整,出生產事故了吧 中有一個地方講的有瑕疵,不知道你們發現了沒有
框住的第一句是沒問題的,但第二句是不夠嚴謹的,我自罰三耳光!
爲什麼這麼說,我們細看下當時的異常堆棧信息
發現了什麼?
在 Mybatis 與 JDBC 之間有 hikari
hikari 是什麼,一個性能極高的數據庫連接池,它是可以有自己的想法的!
思維再擴散一點,格局再打開一點,是不是就是:在 Mybatis 與 JDBC 之間有 數據庫連接池
哪些說不用 數據庫連接池 的小夥伴,你最好想清楚了再說
用關係型數據庫,而不用數據庫連接池的項目多嗎?仔細回憶回憶
回到框住的第二句,嚴謹的說法應該是:而是交由下游組件
至於下游組件是 Hikari ,還是 Druid ,亦或是其他的,是不是都囊括進來了?是不是就嚴謹了?
druid SQLFeatureNotSupportedException
mybatis-plus/issues/1114 中提到了一個異常: java.sql.SQLFeatureNotSupportedException
基於 druid 1.1.16 觸發的異常
我們調整下代碼
1、引入 druid 依賴
2、修改數據源類型(默認的 hikari 可以不用配置 type )
數據庫數據源就已經切成 druid 了
我們將 mysql-connector-java 版本調回到最初的 5.1.26 , Mybatis-Plus 仍使用 3.1.1
運行 com.qsl.OrderTest#orderListAllTest ,此時的異常是什麼,還是 Conversion not supported for type java.time.LocalDateTime 嗎?
org.springframework.dao.InvalidDataAccessApiUsageException: Error attempting to get column 'pay_time' from result set. Cause: java.sql.SQLFeatureNotSupportedException ; null; nested exception is java.sql.SQLFeatureNotSupportedException at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:96) at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) 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.$Proxy54.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.$Proxy60.selectList(Unknown Source) at com.qsl.OrderTest.orderListAllTest(OrderTest.java:33) 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.SQLFeatureNotSupportedException at com.alibaba.druid.pool.DruidPooledResultSet.getObject(DruidPooledResultSet.java:1771) 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.$Proxy73.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
和 hikari 對比下
可以看到,差別還是比較大的
從異常堆棧信息,我們得知,調用棧是: Mybatis -> 數據庫連接池 -> mysql-connector-java
當數據庫連接池是 druid 1.1.16 時,在調用棧的第二環( druid )就異常了
而當數據庫連接池是 hikari 3.4.5 時,在調用棧的第三環才異常,而在第二環( hikari )並未異常
那就來看看 druid 爲何會異常
爲何異常
相信看了 都說了能不動就別動,非要去調整,出生產事故了吧 的小夥伴,能夠很快定位到關鍵代碼
對,你想的沒錯,就是從異常堆棧中找關鍵位置
我們就從 LocalDateTimeTypeHandler.java:38 開始,來看看異常是怎麼產生的
打個斷點,然後再 debug 下
按 F7 ,進入 org.apache.ibatis.logging.jdbc.ResultSetLogger#invoke
紅框框住的代碼,大家能看懂嗎?我給大家拆分下
method 信息如下
rs 對象
method.invoke(rs, params); 作用是不是明顯了?
就是反射調用 DruidPooledResultSet 的方法: getObject(String columnLabel, Class<T> type)
跟進去看下該方法的具體實現
哦豁,直接拋出異常 SQLFeatureNotSupportedException
原因是不是找到了: druid 1.1.16 不支持根據JAVA類型獲取列值
如何修復
1、降低 Mybatis 版本
將 Mybatis 版本降低至 3.5.0 或以下
因爲項目中用的是 Mybatis-Plus ,我們將其降至 3.1.0
運行 com.qsl.OrderTest#orderListAllTest ,沒異常,結果也正確
此時各個組件的版本: Mybatis-Plus 3.1.0 (即 Mybatis 3.5.0 ), druid 1.1.16 , mysql-connector-java 5.1.26
爲何將 Mybatis 的版本調整至 3.5.0 就可以了?
這其實跟 Mybatis 3.5.1 對 LocalDateTimeTypeHandler.java 的調整有關
Mybatis 3.5.0 依賴下游組件的 getTimestamp(String columnLabel) 或 getTimestamp(int columnIndex)
而 Mybatis 3.5.1 依賴下游組件的 getObject(int columnIndex, Class<T> type) 或 getObject(String columnLabel, Class<T> type)
2、升級 druid 版本
升級到哪個版本,這個需要看 druid 從哪個版本開始支持 LocalDateTime
根據官方說明,從 1.1.18 開始支持
我們來看下 1.1.18 相較於上一個版本( 1.1.16 ,沒有 1.1.17 ),對 DruidPooledResultSet 調整了什麼
我們將 Mybatis-Plus 改回成 3.1.1 ( Mybatis 3.5.1 ),然後將 druid 升級到 1.1.18
再執行 com.qsl.OrderTest#orderListAllTest ,你會發現還是有異常!!!
但是先別慌,該異常
是不是很眼熟?
不是在 都說了能不動就別動,非要去調整,出生產事故了吧 已經解決過了嗎?
那怎麼修?
有小夥伴會說:這個我會,將 mysql-connector-java 升級到 5.1.37
就不能一步到位升級到 5.1.42 ?, 5.1.37 有 NullPointerException 呀!!!
此時各個組件的版本: Mybatis-Plus 3.1.1 (即 Mybatis 3.5.1 ), druid 1.1.18 , mysql-connector-java 5.1.42
Hikari 爲何沒問題
此刻相信大家會有一個問題:爲何 hikari 沒有 druid 的那個問題( SQLFeatureNotSupportedException )
我們來分析下,hikari 版本是 3.4.5
它的 HikariProxyResultSet 有實現 getObject(int columnIndex, Class<T> type) 和 getObject(String columnLabel, Class<T> type)
具體實現交給了下游,也就是交給了 mysql-connector-java
那它是一開始就是這麼實現的,還是在 3.4.5 或之前的某個版本調整成這樣的了?
因爲 HikariProxyResultSet 是動態生成的,沒有現成的源代碼,我也想幫你們分析,可我暫時做不到呀
等我瞭解了 HikariCP 動態代理實現機制,我再來給你們分析,暫時算我欠你們的!
你們要實在是覺的不爽,來打我呀
總結
遇到異常不要害怕,異常堆棧是很有用的信息
遇到開源組件的問題, github 上搜它的相關 issue ,往往能事半功倍
還是那句話:能不動就不要動,改好沒績效,改出問題要背鍋,喫力不討好,又不是不能跑