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

我是風箏,公衆號「古時的風箏」,一個不僅有技術的技術公衆號,一個在程序圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。
Spring Cloud 系列文章已經完成,能夠到 個人github 上查看系列完整內容。也能夠在公衆號內回覆「pdf」獲取我精心製做的 pdf 版完整教程。java

寫代碼多年,我一直有個習慣,只要是要作的功能模塊不是很複雜,通常都是上來狂寫一通代碼,等功能作好了,再啓動服務測試,哪裏有問題再改(實話說,單元測試寫的也很少)。而不是寫完一個接口或方法就測試一下,最長的記錄應該是連着寫四、5天代碼,而後一把測試經過,那感受,爽到能夠多吃一碗飯。git

代碼路上的滑鐵盧

然而,就在前兩天,我感受遭遇到了代碼人生的滑鐵盧,其實遇到過不僅一次了,每次滑完鐵,再爬起來慢慢就忘了。此次,我把它寫下來,這樣就不會忘了。github

事情是這樣的,前兩天要對項目加個功能。項目 ORM 採用的是 MyBatis,由於增長了數據庫表,因此要對應的生成 DAO 層和 MyBatis 映射文件(mapper.xml)。因爲對以前業務不是熟悉,我只是先把各個實體類啊、業務類啊、映射文件啊、枚舉類啊等等都建出來,而後寫了兩個簡單接口準備調試一下,因而我點了啓動按鈕,沒問題,沒有一點錯誤,項目正常啓動了,看上去是那麼的完美。spring

我構造了一個請求,打算測一下剛剛寫好的接口,當請求發送出去以後,一個熟悉的異常出如今了 IDEA 控制檯中,invalid bound statement (not found),用過 MyBatis 的同窗恐怕沒有不認識這個異常的,它的意思就是咱們調用 DAO 方法的時候,在 mapper.xml 文件中沒有找到對應的 statement,或者說是沒有找到你定義的 SQL 查詢語句塊。sql

出現這個異常多是下面的這幾個緣由:數據庫

  1. xml 文件的 namespace 和對應的接口名不一致
  2. 接口類中的方法和 xml 文件中的 statement id 對應不上
  3. xml 文件中有中文註釋
  4. 隨意在 xml 文件中加一個空格或者空行而後保存,可能能解決問題

若是你是用工具自動生成 xml 還好,若是是手動建立的,那極可能因爲疏忽出現這個問題,好比咱們從另外一個文件複製過來,忘記改 namespace 了,或者接口方法名和 statement id 差了一個字母或者字母順序不一致。這個異常是很使人頭疼的,就好比相差一個字母這種狀況,很難被發現,因此最好仍是寫好接口方法名,而後複製到 xml 中。數組

我雖然有段時間沒有碰 MyBatis 了,做爲一個老司機,我碰到這個問題其實一點也不慌,由於雖然是工具自動生成的 xml 文件,可是我確實又加了幾個 statement 塊兒,並且 id 也是手敲的,而且報錯的確實也是我手動加上的,因此,我猜想應該是名字沒對上,敲錯字母或者順序不一致,因而我進去排查了一下,可是沒發現什麼問題,爲了保險起見,我又到接口中把方法名字複製到 xml 中了,而後肯定 namespace 沒問題,沒有中文註釋,而且又在 xml 中加了個空行(雖然歷來沒用這個方法解決過問題),而後從新啓動項目,可是,異常仍是沒有消失。mybatis

及時跳出來,不要陷在裏面

這就有點奇怪了,又從新檢查了一遍,沒錯,都正常,看不出問題所在。當肯定沒有問題的時候,就要跳出來了,得從其餘方向或者更高層次考慮一下了,否則極可能就陷在裏面了。劃重點,這是屢次教訓總結出來的規律。我能夠肯定當前調用的這個接口方法和 statement 都徹底沒有問題,那頗有多是別的問題,會不會是這個 xml 文件沒有被編譯打包進去,因而我進到 target 目錄查探一番,有的,並且查看內容,肯定是沒有問題的。app

有時候問題很奇怪,可能和 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 的斜槓開發者。能夠在公衆號中加我好友,進羣裏小夥伴交流學習,好多大廠的同窗也在羣內呦。

相關文章
相關標籤/搜索