一個排查了大半天兒的問題,差點又讓 MyBatis 背鍋 一個排查了大半天兒的問題,差點又讓 MyBatis 背鍋

摘自:https://www.cnblogs.com/fengzheng/p/12907122.html

一個排查了大半天兒的問題,差點又讓 MyBatis 背鍋

 

我是風箏,公衆號「古時的風箏」,一個不只有技術的技術公衆號,一個在程序圈混跡多年,主業 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 查詢語句塊。

出現這個異常可能是下面的這幾個原因:

  1. xml 文件的 namespace 和對應的接口名不一致
  2. 接口類中的方法和 xml 文件中的 statement id 對應不上
  3. xml 文件中有中文註釋
  4. 隨意在 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這個包有問題。於是我又試瞭如下幾個方法:

  1. 把這個有問題的包路徑放到第一個,無效。
  2. 把其他兩個註釋,只留這個有問題的,無效。
  3. 難道是 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 的斜槓開發者。可以在公衆號中加我好友,進羣裏小夥伴交流學習,好多大廠的同學也在羣內呦。

人生沒有回頭路,珍惜當下。
 
分類: java
標籤: mybatis
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章