上周一好友向我反馈一个问题,他们项目在本地是可以跑的,但是在线上环境,就报错.报错日志如下:
Could not find result map cn.mycs.server.persistence.dao.UserMapper.BaseResultMap
说实话,我每天这么忙,看到这种直接丢个异常出来的根本不想理.但是他一句话彻底改变了我的想法.
首先出现了这个几个关键词.
无法解决的bug
之前 肥朝反复强调 ,我们看源码,是为了解决问题,而不是简单为了面试装装逼,如果搜索引擎随便搜索第一页都能解决,那还看源码真的是 风骚走位完美避开了最高效的解决问题方式
特定环境出现
从聊天记录中可以看出,该问题还受到环境的条件限制,不方便模拟,最关键是肥朝还不能直接连上他们公司的环境去帮他看问题.
事出反常必有妖,加上他是肥朝 公众号粉丝 (划重点),那就只能来一波捉妖记了!
其实很多人写了几年代码之后都常常感叹,写代码真的好容易,就是用各种框架,堆积木式编程.其实他们之所以有这样的感叹,主要是工作中遇到的挑战还不够多.以至于他们认为 原理
、 源码
这些东西纯粹只是面试装逼.
当然会看源码解决搜索引擎无法解决问题,还是远远不够的.高并发下.会出现很多难以重现的问题,这个时候,必须要学会一个新的技能,就是通过日志,通过 眼神编译 , 静态看源码
.
因此,我询问得到了报错日志如下:
从报错日志中可以看出,这个报错还和 Mybatis plus
有关,但是肥朝没用过什么 Mybatis plus
啊,这可如何是好?没关系,前面都说了,静态看源码, 眼神编译!
,于是我开始新建一个demo,引入相关的依赖.
将上图的异常栈再标记一下重点
从我标记的三个重点加上小学简单的英文就可以看出,在解析 UserPersonalMapper.xml
时,没有找到 BaseResultMap
.另外一点,从我标记中的重点中也可以看出,这个 BaseResultMap
是在另外一个XML,也就是 UserMapper.xml
中声明的.
这个时候可能就有朋友想到,那是不是加载 UserPersonalMapper.xml
的时候, UserMapper.xml
还没加载导致的呢.导致无法找到 UserMapper.xml
定义的 BaseResultMap
坦白说,这个猜测,一点毛病都没有,非常合情合理
但是最关键的是,本地跑是没问题的.那为什么我本地跑的时候,又没有报错,这个你又怎么解释?
很多朋友都问到我怎么看源码,那么我现在就手把手,根据仅有的线索,九浅一深直入源码.
日志告诉我们是583行的时候报错的(图中已圈),然后 mapperLocations
也很明显,就是我们配置我mapper集合,他就是从这里集合中遍历出每一个mapper来进行解析的.
那么关键问题来了,我们现在是静态看代码,眼神编译,我们打不了断点,那么这个mapper是什么时候set进去的,set了哪些值呢?为了做到毫不保留向公众号粉丝传输心路历程,我就详细截图一下.
以下几个技巧完全是 IDEA
的使用问题
1.查看变量在哪里被引用
2.查看方法在哪里被调用
终于,让我们找到了核心处理逻辑
从 resolveMapperLocations
和 PathMatchingResourcePatternResolver
这两个类名和返回值 Resource[]
,哪怕是把单词拆开一个一个翻译都大概能猜出,这个是根据配置的 /*.xml
这种配置,找到所有的xml资源. Resource[]
是数组,数组是有序的,所以这个数组中的元素(mapper)顺序的顺序,就能决定我们前面的猜想是不是正确的.
于是我就叫该好友添加上这段日志,验证一下猜想
然后他把能正常启动的日志和异常的日志发出来,如下
我们发现,果然如我们所料,这个加载的顺序果然有问题,异常启动的, UserMapper.xml
在最后才加载,自然导致遍历的时候最后才解析到这个xml,所以这个xml上定义的 BaseResultMap
不能被之前加载的使用
但是关键问题是,还是没说清楚,为什么本地就没问题.为什么本地跑加载的顺序就OK了呢?
现在范围已经很小了,我们从前面的猜想,到验证猜想,已经把目标逐步缩小,现在问题就只剩下一个,只要弄清楚 PathMatchingResourcePatternResolver
的逻辑,一切就豁然开朗了
因为线上环境,都是打成jar依赖启动的.而本地走的是classes,所以他们走的代码分支是不一样的
看问题,一定要经过深度思考.比如这里应该有的疑问是,为什么肥朝就知道.他们走的分支不一样.再说男人的山盟海誓都是假的,我怎么知道肥朝说的是不是真的.
坦白说,关注肥朝最好的时间是在两年前,其次是现在,如果你从源码解析系列文章就开始就关注肥朝,练就了 眼神编译,静态看源码
的能力,这里自然不会有这个疑问
如果不幸错过了最好的关注时机,我再给你一个猥琐的办法.你把 PathMatchingResourcePatternResolver
这个类拷贝出来,改个名字.比如 FeichaoPathMatchingResourcePatternResolver
.然后打上一些简陋的日志信息,如下图
然后你本地启动,和jar启动,看日志输出.
那么,这两个分支究竟有什么区别呢?这个本地代码走的代码分支,有一段很重要的逻辑
他这里会根据资源文件进行排序,那么到底根据什么规则排序
我们拿出肥朝之前文章里介绍的常用开发工具来看一眼.
此时豁然开朗.为什么本地一直没有重现?因为本地跑的时候,他走的代码逻辑已经把资源文件默认按照文件名排序了,导致了这个 UserMapper.xml
一直在 UserPersonalMapper.xml
之前加载.
坦白说,其实这种两个 mapper
引用的做法我认为是非常不规范的.因为资源文件打进jar的 文件排序 是非常不可控的,他可能会因为构建工具的不同比如 Maven
、 Gradle
,甚至还会因为构建工具的版本,比如Maven的版本,还有可能受到环境,比如window、linux等等的影响.之所以本地开发时候没有发现问题,纯属是偶然成功.我们最后再来用简单的 JDK Api
验证一下问题.我们拿一个能正常启动和报错的jar
很明显,这两个jar中的资源文件顺序是不一样的.所以如果按照他的这种 mapper
写法,可能会出现因为A环境的maven版本比较高,做了优化等因素,默认按照文件名顺序打入jar中,但是另一个B环境,打入jar的资源是无序的.到时候问题描述可能就变成了很神秘的"一样的代码,A环境打包运行一直是成功的,B环境打包偶然性运行失败".这就是我在团队经常提到的一个名词,偶然成功.