[肥朝]本地可跑,上线就崩?慌了!

3,837 阅读7分钟

前言

上周一好友向我反馈一个问题,他们项目在本地是可以跑的,但是在线上环境,就报错.报错日志如下:

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.查看方法在哪里被调用

终于,让我们找到了核心处理逻辑

resolveMapperLocationsPathMatchingResourcePatternResolver这两个类名和返回值Resource[],哪怕是把单词拆开一个一个翻译都大概能猜出,这个是根据配置的/*.xml这种配置,找到所有的xml资源.Resource[]是数组,数组是有序的,所以这个数组中的元素(mapper)顺序的顺序,就能决定我们前面的猜想是不是正确的.

验证猜想

于是我就叫该好友添加上这段日志,验证一下猜想

然后他把能正常启动的日志和异常的日志发出来,如下

我们发现,果然如我们所料,这个加载的顺序果然有问题,异常启动的,UserMapper.xml在最后才加载,自然导致遍历的时候最后才解析到这个xml,所以这个xml上定义的BaseResultMap不能被之前加载的使用

但是关键问题是,还是没说清楚,为什么本地就没问题.为什么本地跑加载的顺序就OK了呢?

深入浅出

现在范围已经很小了,我们从前面的猜想,到验证猜想,已经把目标逐步缩小,现在问题就只剩下一个,只要弄清楚PathMatchingResourcePatternResolver的逻辑,一切就豁然开朗了

因为线上环境,都是打成jar依赖启动的.而本地走的是classes,所以他们走的代码分支是不一样的

深入思考

看问题,一定要经过深度思考.比如这里应该有的疑问是,为什么肥朝就知道.他们走的分支不一样.再说男人的山盟海誓都是假的,我怎么知道肥朝说的是不是真的.

坦白说,关注肥朝最好的时间是在两年前,其次是现在,如果你从源码解析系列文章就开始就关注肥朝,练就了眼神编译,静态看源码的能力,这里自然不会有这个疑问

如果不幸错过了最好的关注时机,我再给你一个猥琐的办法.你把PathMatchingResourcePatternResolver这个类拷贝出来,改个名字.比如FeichaoPathMatchingResourcePatternResolver.然后打上一些简陋的日志信息,如下图

然后你本地启动,和jar启动,看日志输出.

那么,这两个分支究竟有什么区别呢?这个本地代码走的代码分支,有一段很重要的逻辑

他这里会根据资源文件进行排序,那么到底根据什么规则排序

我们拿出肥朝之前文章里介绍的常用开发工具来看一眼.

此时豁然开朗.为什么本地一直没有重现?因为本地跑的时候,他走的代码逻辑已经把资源文件默认按照文件名排序了,导致了这个UserMapper.xml一直在UserPersonalMapper.xml之前加载.

规范开发,拒绝偶然成功

坦白说,其实这种两个mapper引用的做法我认为是非常不规范的.因为资源文件打进jar的文件排序是非常不可控的,他可能会因为构建工具的不同比如MavenGradle,甚至还会因为构建工具的版本,比如Maven的版本,还有可能受到环境,比如window、linux等等的影响.之所以本地开发时候没有发现问题,纯属是偶然成功.我们最后再来用简单的JDK Api验证一下问题.我们拿一个能正常启动和报错的jar

很明显,这两个jar中的资源文件顺序是不一样的.所以如果按照他的这种mapper写法,可能会出现因为A环境的maven版本比较高,做了优化等因素,默认按照文件名顺序打入jar中,但是另一个B环境,打入jar的资源是无序的.到时候问题描述可能就变成了很神秘的"一样的代码,A环境打包运行一直是成功的,B环境打包偶然性运行失败".这就是我在团队经常提到的一个名词,偶然成功.

写在最后

肥朝 是一个专注于 原理、源码、开发技巧的技术公众号,号内原创专题式源码解析、真实场景源码原理实战(重点)。扫描下面二维码关注肥朝,让本该造火箭的你,不再拧螺丝!