此次學習MyBatis的主題我想記錄一個使用起來可能會遇到,可是沒有經驗的話很很差解決的BUG,在特定狀況下很容易發生.java
java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.xxx.package.ClassA.fun1mybatis
這個錯誤很是很是常見,百度一下大部分問題都能解決.這個問題90%出現的緣由是一個Mapper文件中有2個節點的id相同就會出現. 只要仔細檢查下id就OK了. app
可能不少問題Mybatis都會報這個錯,id重複是其中之一,也是最多見的,這點上來講MyBatis的錯誤提示不夠好.ide
如今我來分享一種狀況也會出現這個錯誤,可是不是id重複的狀況.學習
1.用MyBatis Generator自動生成Mapper和XML,而且ui
<javaClientGenerator targetPackage="" targetProject="" type="MIXEDMAPPER"/>
type="MIXEDMAPPER" 也就是自定生成的文件是混用註解和XML的時候有必定機率產生.idea
這種生成下Mapper中的select語句有註解@ResultMap, 同時resultMap是定義在XML中的.spa
2.由於通常項目自動生成的XML和本身手寫的會分開,因此會有多個XML. 而且本身定義的XML中也有resultMap.debug
3.一點點運氣,這個和XML加載順序有關係,在idea沒有打成jar包,也就是文件是按文件名排序的時候不會有機率問題(開發debug加載順序是肯定的),可是mapper.XML被打成jar在jar裏的時候加載順序不是按照字母順序,(發佈打成jar發佈到生產上的時候看起來是亂序)的時候就有必定機率發生(不是必現錯誤,須要看當時XML的加載順序)code
正如以前文章分享的那樣,MyBatis在啓動的時候會讀取Mapper XML去解析生產MapperedStatement.
假設我有1個Mapper A, 對應XML1和XML2. 2個XML文件.
A.fun1方法上有@ResultMap註解,在XML2中定義對應的resultMap.
啓動的時候若是先讀取到了XML1這個XML. 這個時候會調用XMLMapperBuilder.parse方法去解析
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
其中須要調用bindMapperForNamespace. 也就是解析namespace.這步操做會找到namespace對應的Mapper.若是Mapper沒有被configuration加載過,就先去加載這個Mapper. 請注意沒有被加載過的時候纔回去加載這個Mapper.
如上圖, hasMapper方法若是檢測到沒有加載過就調用configuration.addMapper方法
內部會調用mapperRegistry的addMapper方法.其中會調用MapperAnnotationBuilder去解析Mapper這個類.至關於解析你寫的Mapper接口
這個是作什麼操做呢? 當解析到你的select方法上有@resultMap的時候MyBatis須要找到這個select是對應到了哪一個resultMap?
可是你的自定義的resultMap在XML2文件中,尚未加載.因此這個時候這個select方法會被添加到incompleteMethods中.至關於標記這個方法是沒有解析完成的,由於目前信息不夠.
而parsePendingMethods就是會去檢查全部的incompleteMethods,若是能夠解析了.那就移除.
也就是說新加載的Mapper結束的時候都會調用parsePendingMethods檢查未完成Methods.
那麼這個BUG何時回發生呢? 就在於你的XML2這個文件是最後1個加載的Mapper XML.這個時候由於Mapper類在XML1的時候已經加載過了.因此不會進MapperRegistry的addMapper方法,也就不會作parsePendingMethods方法.而這個XML後面沒有其餘XML了..因此解析至此就結束了.那麼XML裏對應的Mapper中自定義@resultMap對應的那個select就回一直存在於incompleteMethods..mybatis初始化完成的時候儘管incompleteMethods的size不爲0,可是這個時候還不會報錯.
那麼是哪裏報錯的呢?
是當你調用你的任意一個mapper.XXX方法的時候. 由於這個時候首先要生成MethodProxy. MethodProxy要生成MappedStatement.
如上圖,MappedStatement就至關因而你XML裏select等節點的java表示. 首先hasStatement是要檢查一下這個Mapper.XXX方法在XML裏到底有沒有對應的SQL.沒有確定不能作下去.由於找不到SQL.
找到了就會調getMappedStatement. 這2個方法都會調用1個特殊的方法
如上圖所示, 看buildAllstatements的註釋也能明白他是幹嗎的.就是檢查是否有未完成的配置,incompleteXXX中若是有對象,就調用他的resolve方法去作解析.
這個時候由於全部配置加載完成了.因此以前沒有解析的那個ClassA.fun1會被添加到mappedStatements中. 可是! incompleteMethods中的對象並無清除. 這點我以爲很奇怪...resolve()之後不是應該清除對象嗎..
像初始化的各類parsePending方法中若是resolve了都會清除incompleteXXX中的對象的..可是這裏卻沒有remove...不知道爲何(以下圖)
也就是說調用hasStatement的時候會向confioguration的mappedStatements中添加ClassA.fun1
而調用getMappedStatement的時候又會添加一次
而 mappedStatements在configuration中是這麼聲明的:
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>
如上圖, 在重複添加key的時候就會報錯了.
第一.最簡單的.把那個@ResultMap的select方法的SQL寫道XML裏而不是@Select寫在類上.也就是說select語句和resultMap是一塊兒加載的.
或者
第二.不要使用自定義resultMap..不少狀況下能夠被@ResultType代替.