MyBatis 学习记录7 一个Bug引发的思考
主题
这次学习MyBatis的主题我想记录一个使用起来可能会遇到,但是没有经验的话很不好解决的BUG,在特定情况下很容易发生.
异常
java.lang.IllegalArgumentException: Mapped Statements collection already contains value for com.xxx.package.ClassA.fun1
这个错误非常非常常见,百度一下大部分问题都能解决.这个问题90%出现的原因是一个Mapper文件中有2个节点的id相同就会出现. 只要仔细检查下id就OK了.
可能很多问题Mybatis都会报这个错,id重复是其中之一,也是最常见的,这点上来说MyBatis的错误提示不够好.
现在我来分享一种情况也会出现这个错误,但是不是id重复的情况.
1.用MyBatis Generator自动生成Mapper和XML,并且
<javaClientGenerator targetPackage="" targetProject="" type="MIXEDMAPPER"/>
type="MIXEDMAPPER" 也就是自定生成的文件是混用注解和XML的时候有一定概率产生.
这种生成下Mapper中的select语句有注解@ResultMap, 同时resultMap是定义在XML中的.
2.因为一般项目自动生成的XML和自己手写的会分开,所以会有多个XML. 并且自己定义的XML中也有resultMap.
3.一点点运气,这个和XML加载顺序有关系,在idea没有打成jar包,也就是文件是按文件名排序的时候不会有概率问题(开发debug加载顺序是确定的),但是mapper.XML被打成jar在jar里的时候加载顺序不是按照字母顺序,(发布打成jar发布到生产上的时候看起来是乱序)的时候就有一定概率发生(不是必现错误,需要看当时XML的加载顺序)
原理
正如之前文章分享的那样,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代替.
MyBatis 学习记录7 一个Bug引发的思考的更多相关文章
- MyBatis 学习记录1 一个简单的demo
主题 最近(N个月前)clone了mybatis的源码..感觉相比于spring真的非常小...然后看了看代码觉得写得很精简...感觉我的写代码思路和这个框架比较相似(很难具体描述...就是相对来说比 ...
- Spring之LoadTimeWeaver——一个需求引发的思考---转
原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver——一个需求引 ...
- 由一个emoji引发的思考
由一个emoji引发的思考 从毕业以来,基本就一直在做移动端,但是一直就关于移动端的开发,各种适配问题的解决,在日常搬砖中处理了就过了,也没有把东西都沉淀下来,觉得甚是寒颜.现就一个小bug,让我们来 ...
- MyBatis 学习记录5 MyBatis的二级缓存
主题 之前学习了一下MyBatis的一级缓存,主要涉及到BaseExecutor这个类. 现在准备学习记录下MyBatis二级缓存. 配置二级缓存与初始化发生的事情 首先二级缓存默认是不开启的,需要自 ...
- z-index失效原因分析——由一个bug引发的对层叠上下文和z-index属性的深度思考
新年刚开工就被一个bug虐得整个人都不好了,特地记录下. (一)bug描述 在一个fixed-data-table(一个React组件)制作的表格中,需要给表头的字段提示的特效,所以做了一个提示层,但 ...
- 由一个bug引发的SQLite缓存一致性探索
问题 我们在生产环境中使用SQLite时中发现建表报“table xxx already exists”错误,但DB文件中并没有该表.后面才发现这个是SQLite在实现过程中的一个bug,而这个bug ...
- Mybatis学习记录(五)----Mybatis的动态SQL
1. 什么是动态sql mybatis核心 对sql语句进行灵活操作,通过表达式进行判断,对sql进行灵活拼接.组装. 1.1 需求 用户信息综合查询列表和用户信息查询列表总数这两个statemen ...
- MyBatis 学习记录3 MapperMethod类
主题 之前学习了一下MapperProxy的生产过程,自定义Mapper类的对象是通过动态代理生产的,调用自定义方法的时候实际上是调用了MapperMethod的execute方法:mapperMet ...
- Mybatis学习记录(1)
1.Mybatis介绍 Mybatis是apache的一个开源项目iBatis,Mybatis是一个优秀的持久层框架,他对jdbc的操作数据库的过程进行封装,使开发者只需要关注sql本身,不需 ...
随机推荐
- Codeforces 828C String Reconstruction【并查集巧妙运用】
LINK 题目大意 给你n个串和在原串中的出现位置,问原串 思路 直接跑肯定是GG 考虑怎么优化 因为保证有解,所以考虑过的点我们就不再考虑 用并查集维护当前每个点之后最早的没有被更新过的点 然后就做 ...
- 20179223《Linux内核原理与分析》第十一周学习笔记
缓冲区溢出漏洞实验 一.实验简介 缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况.这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段.这一漏洞的出现是由于数据缓冲器和 ...
- 自动化测试时Ios设备无法调出键盘问题
- 在编写异步方法时,使用 ConfigureAwait(false) 避免使用者死锁
我在 使用 Task.Wait()?立刻死锁(deadlock) 一文中站在类库使用者的角度看 async/await 代码的死锁问题:而本文将站在类库设计者的角度来看死锁问题. 阅读本文,我们将知道 ...
- RSA 每次公钥加密不同结果
今天服务器端一哥们突然跑过来跟我说:我发现公钥每次加密都不同结果啊? 我说:怎么可能?不同的话,私要怎么解密和验证啊? 然后我屁颠屁颠的试了下,结果发现不论在在线RSA的还是自己公司 利用同一个明文加 ...
- [Oracle] CPU/PSU补丁安装详细教程
Oracle CPU的全称是Critical Patch Update, Oracle对于其产品每个季度发行一次安全补丁包,通常是为了修复产品中的安全隐患,以下是对CPU/PSU补丁安装的具体操作步骤 ...
- NanoPiM1开箱测试
等了快一周了,终于那M1与那外壳一起给我寄过来了. 上午收到,开箱图就不亮了,来一上好电的图! 一同购买来的MSD卡里什么也没有,上电测试时只看到绿色的灯微微亮(这是一个BUG吗!!!!哈哈). 所以 ...
- cocos2d-x的popScene的动画效果
找到CCDirector.h,找到void popScene(); 在下面加上一段类模板 template <class T> void popSceneWithTransition(fl ...
- adb命令记录
1.杀掉 adb 进程 adb kill-server 2.重启 adb 服务 adb start-server 3.重启手机 adb reboot 4.进 shell 模 ...
- [转]Oracle 连接dll
https://www.nuget.org/packages/Oracle.ManagedDataAccess