我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。
Spring Cloud 系列文章已經完成,可以到 我的github 上檢視系列完整內容。也可以在公眾號內回覆「pdf」獲取我精心製作的 pdf 版完整教程。
寫程式碼多年,我一直有個習慣,只要是要做的功能模組不是很複雜,一般都是上來狂寫一通程式碼,等功能做好了,再啟動服務測試,哪裡有問題再改(實話說,單元測試寫的也不多)。而不是寫完一個介面或方法就測試一下,最長的記錄應該是連著寫4、5天程式碼,然後一把測試通過,那感覺,爽到可以多吃一碗飯。
程式碼路上的滑鐵盧
然而,就在前兩天,我感覺遭遇到了程式碼人生的滑鐵盧,其實遇到過不只一次了,每次滑完鐵,再爬起來慢慢就忘了。這次,我把它寫下來,這樣就不會忘了。
事情是這樣的,前兩天要對專案加個功能。專案 ORM 採用的是 MyBatis,因為增加了資料庫表,所以要對應的生成 DAO 層和 MyBatis 對映檔案(mapper.xml)。由於對之前業務不是熟悉,我只是先把各個實體類啊、業務類啊、對映檔案啊、列舉類啊等等都建出來,然後寫了兩個簡單介面準備除錯一下,於是我點了啟動按鈕,沒問題,沒有一點錯誤,專案正常啟動了,看上去是那麼的完美。
我構造了一個請求,打算測一下剛剛寫好的介面,當請求傳送出去之後,一個熟悉的異常出現在了 IDEA 控制檯中,invalid bound statement (not found)
,用過 MyBatis 的同學恐怕沒有不認識這個異常的,它的意思就是我們呼叫 DAO 方法的時候,在 mapper.xml 檔案中沒有找到對應的 statement,或者說是沒有找到你定義的 SQL 查詢語句塊。
出現這個異常可能是下面的這幾個原因:
- xml 檔案的 namespace 和對應的介面名不一致
- 介面類中的方法和 xml 檔案中的 statement id 對應不上
- xml 檔案中有中文註釋
- 隨意在 xml 檔案中加一個空格或者空行然後儲存,可能能解決問題
如果你是用工具自動生成 xml 還好,如果是手動建立的,那很可能由於疏忽出現這個問題,比如我們從另一個檔案複製過來,忘記改 namespace 了,或者介面方法名和 statement id 差了一個字母或者字母順序不一致。這個異常是很令人頭疼的,就比如相差一個字母這種情況,很難被發現,所以最好還是寫好介面方法名,然後複製到 xml 中。
我雖然有段時間沒有碰 MyBatis 了,作為一個老司機,我碰到這個問題其實一點也不慌,因為雖然是工具自動生成的 xml 檔案,但是我確實又加了幾個 statement 塊兒,而且 id 也是手敲的,並且報錯的確實也是我手動加上的,所以,我猜測應該是名字沒對上,敲錯字母或者順序不一致,於是我進去排查了一下,但是沒發現什麼問題,為了保險起見,我又到介面中把方法名字複製到 xml 中了,然後確定 namespace 沒問題,沒有中文註釋,並且又在 xml 中加了個空行(雖然從來沒用這個方法解決過問題),然後重新啟動專案,但是,異常還是沒有消失。
及時跳出來,不要陷在裡面
這就有點奇怪了,又重新檢查了一遍,沒錯,都正常,看不出問題所在。當確定沒有問題的時候,就要跳出來了,得從其他方向或者更高層次考慮一下了,不然很可能就陷在裡面了。劃重點,這是多次教訓總結出來的規律。我可以確定當前呼叫的這個介面方法和 statement 都完全沒有問題,那很有可能是別的問題,會不會是這個 xml 檔案沒有被編譯打包進去,於是我進到 target 目錄查探一番,有的,而且檢視內容,確定是沒有問題的。
有時候問題很奇怪,可能和 IDE 有關,於是我用 mvn clean
命令清理了一下,然後重新執行,但是,問題依舊在。
接下來,我又試了刪除這個 xml ,然後新建了一個,但是,問題依舊。
再往外跳,你不是這個方法有問題嗎, 那我再新建一個方法,就寫一條最簡單的 SQL,方法名也起的簡單一點,看看會不會有問題,結果,發現新大陸了,這個新建的方法也報這個錯誤。那就有了新的排查方向了,我再試試別的介面中的方法呢,結果,這個包名下的幾個方法,全都有這個錯誤,而其他包名下的方法則沒有問題,因為不同功能的 xml 檔案放在不同的包下,也就是不同的路徑下。
那我就知道了,是 xml 檔案掃描出問題了,肯定是 MyBatis 配置的 mapperLocations
有問題了,有可能是被我或者其他同事不小心多敲了個字母之類的。於是開啟配置檔案看了一下,
mybatis:
mapperLocations: com/xxx/aaa/mapper/*.xml,com/xxx/aaa/bbb/mapper/*.xml,com/xxx/aaa/ccc/mapper/*.xml
MyBatis 配置 mapperLocations
配置了三個包路徑,也就是從這三個包中尋找 *.xml
去解析,但是經過檢查發現,並沒有問題,配置檔案沒有 git 提交記錄,而且配置的包路徑也是正確無誤的,其他兩個包都掃描正常,就是 com/xxx/aaa/ccc/mapper/*.xml
這個包有問題。於是我又試瞭如下幾個方法:
- 把這個有問題的包路徑放到第一個,無效。
- 把其他兩個註釋,只留這個有問題的,無效。
- 難道是 MyBatis 讀取了其他地方的配置?於是我把這個配置註釋掉,結果都出問題了,說明就是讀的這個配置。
原始碼大法好
此時,已經過去很長時間了,問題變的越來越詭異,但是事出必有因,肯定是某些地方出現了問題。實在找不出專案本身的問題了,沒辦法,我只能懷疑是 MyBatis 有問題了,也許真的是觸發了 MyBatis 本身的隱藏 bug。
不到萬不得已是不會用這種方式的,那就是除錯 MyBatis 原始碼。想來,MyBatis 原始碼我還是比較熟悉的。那我們們就再會一會吧。
mybatis-spring-boot-starter 只有三個 Java 檔案,其中 MybatisAutoConfiguration
是關鍵業務類。
而我們知道 MyBatis 中 SqlSessionFactory
是非常核心的物件,所以我們就把斷點加在 sqlSessionFactory(DataSource dataSource)
這個方法上。
如果是第一次除錯開源框架原始碼,往往不能一下子找準位置,其實沒有關係,把斷點打在任何一個位置都可以,大不了就慢慢跟兩遍嘛,本身讀原始碼、除錯的過程就是個漫長的過程。
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 省略...
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
以上程式碼我只保留了本次問題相關的程式碼,那就是解析 mapperLocations 的過程,也就是上面程式碼中this.properties.resolveMapperLocations()
這個方法。
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<Resource> resources = new ArrayList<Resource>();
if (this.mapperLocations != null) {
for (String mapperLocation : this.mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
當我繼續跟蹤程式碼的時候,發現 MyBatis 確實已經識別到了配置檔案中的那三個包路徑,this.mapperLocations
就是那三個包路徑的陣列集合。
接著往下跟,在方法 resourceResolver.getResources(mapperLocation)
中對每一個路徑進行解析,發現前兩個包都正常返回了Resource[]
,也就是對應的 xml 檔案資源,而最後一個返回的確實空陣列,問題原因已經很近了。
接著再次啟動除錯,當解析最後一個包路徑是,進入resourceResolver.getResources(mapperLocation)
方法內部,看看裡面都幹了什麼,最後發現在呼叫以下程式碼之後,返回的 rootDirURL 是一個絕對路徑,也就是 xml 所在的物理路徑。
URL rootDirURL = rootDirResource.getURL();
這時,終於發現問題所在了,這個絕對路徑竟然不是 xml 所在的路徑,而是另外一個子模組下的路徑,經過對比發現,原來,子模組中被新建了一個名稱一樣的資料夾,造成存在兩個完全一樣的包路徑,而以上程式碼返回了另一個包的絕對路徑。於是,聯絡同事,問清楚這個包被建立的原因,發現是最近新加的但是已經廢棄無用的,於是刪掉解決了問題。
正常專案開發中應該可以規避這種問題,模組與模組不應該出現相同包名,應該遵循如下命名:
模組A:com.kite.moduleA
模組B: com.kite.moduleB
這樣從根本上解決問題,以防出現不必要的麻煩。
最後
MyBatis 的這個異常確實令人頭疼,因為錯誤原因不明顯,以此類推,凡是 xml 檔案造成的問題都不太容易排查,大部分情況都是人為疏忽造成的,而錯誤一般都比較隱蔽。
當一個問題經過多方驗證都沒辦法被發現被解決的時候,往往就需要換個思路了,及時跳出來,從其它角度或者更高層次重新審視問題,也許能更快的找到問題原因。
在用開源框架的時候,如果出現問題,長時間找不到解決辦法,那麼可以嘗試除錯一下原始碼,並沒有想象的那麼困難。
壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!
我是風箏,公眾號「古時的風箏」,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的很 6 的斜槓開發者。可以在公眾號中加我好友,進群裡小夥伴交流學習,好多大廠的同學也在群內呦。