此次学习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代替.