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

風的姿態發表於2020-05-18

我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 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 的斜槓開發者。可以在公眾號中加我好友,進群裡小夥伴交流學習,好多大廠的同學也在群內呦。

相關文章