前言
上周一好友向我反馈一个问题,他们项目在本地是可以跑的,但是在线上环境,就报错.报错日志如下:
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环境打包偶然性运行失败".这就是我在团队经常提到的一个名词,偶然成功.