我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。

Spring Cloud 系列文章已经完成,可以到 我的github 上查看系列完整内容。也可以在公众号内回复「pdf」获取我精心制作的 pdf 版完整教程。

写代码多年,我一直有个习惯,只要是要做的功能模块不是很复杂,一般都是上来狂写一通代码,等功能做好了,再启动服务测试,哪里有问题再改(实话说,单元测试写的也不多)。而不是写完一个接口或方法就测试一下,最长的记录应该是连着写4、5天代码,然后一把测试通过,那感觉,爽到可以多吃一碗饭。

代码路上的滑铁卢

然而,就在前两天,我感觉遭遇到了代码人生的滑铁卢,其实遇到过不只一次了,每次滑完铁,再爬起来慢慢就忘了。这次,我把它写下来,这样就不会忘了。

事情是这样的,前两天要对项目加个功能。项目 ORM 采用的是 MyBatis,因为增加了数据库表,所以要对应的生成 DAO 层和 MyBatis 映射文件(mapper.xml)。由于对之前业务不是熟悉,我只是先把各个实体类啊、业务类啊、映射文件啊、枚举类啊等等都建出来,然后写了两个简单接口准备调试一下,于是我点了启动按钮,没问题,没有一点错误,项目正常启动了,看上去是那么的完美。

我构造了一个请求,打算测一下刚刚写好的接口,当请求发送出去之后,一个熟悉的异常出现在了 IDEA 控制台中,invalid bound statement (not found),用过 MyBatis 的同学恐怕没有不认识这个异常的,它的意思就是我们调用 DAO 方法的时候,在 mapper.xml 文件中没有找到对应的 statement,或者说是没有找到你定义的 SQL 查询语句块。

出现这个异常可能是下面的这几个原因:

  1. xml 文件的 namespace 和对应的接口名不一致
  2. 接口类中的方法和 xml 文件中的 statement id 对应不上
  3. xml 文件中有中文注释
  4. 随意在 xml 文件中加一个空格或者空行然后保存,可能能解决问题

如果你是用工具自动生成 xml 还好,如果是手动创建的,那很可能由于疏忽出现这个问题,比如我们从另一个文件复制过来,忘记改 namespace 了,或者接口方法名和 statement id 差了一个字母或者字母顺序不一致。这个异常是很令人头疼的,就比如相差一个字母这种情况,很难被发现,所以最好还是写好接口方法名,然后复制到 xml 中。

我虽然有段时间没有碰 MyBatis 了,作为一个老司机,我碰到这个问题其实一点也不慌,因为虽然是工具自动生成的 xml 文件,但是我确实又加了几个 statement 块儿,而且 id 也是手敲的,并且报错的确实也是我手动加上的,所以,我猜测应该是名字没对上,敲错字母或者顺序不一致,于是我进去排查了一下,但是没发现什么问题,为了保险起见,我又到接口中把方法名字复制到 xml 中了,然后确定 namespace 没问题,没有中文注释,并且又在 xml 中加了个空行(虽然从来没用这个方法解决过问题),然后重新启动项目,但是,异常还是没有消失。

及时跳出来,不要陷在里面

这就有点奇怪了,又重新检查了一遍,没错,都正常,看不出问题所在。当确定没有问题的时候,就要跳出来了,得从其他方向或者更高层次考虑一下了,不然很可能就陷在里面了。划重点,这是多次教训总结出来的规律。我可以确定当前调用的这个接口方法和 statement 都完全没有问题,那很有可能是别的问题,会不会是这个 xml 文件没有被编译打包进去,于是我进到 target 目录查探一番,有的,而且查看内容,确定是没有问题的。

有时候问题很奇怪,可能和 IDE 有关,于是我用 mvn clean 命令清理了一下,然后重新运行,但是,问题依旧在。

接下来,我又试了删除这个 xml ,然后新建了一个,但是,问题依旧。

再往外跳,你不是这个方法有问题吗, 那我再新建一个方法,就写一条最简单的 SQL,方法名也起的简单一点,看看会不会有问题,结果,发现新大陆了,这个新建的方法也报这个错误。那就有了新的排查方向了,我再试试别的接口中的方法呢,结果,这个包名下的几个方法,全都有这个错误,而其他包名下的方法则没有问题,因为不同功能的 xml 文件放在不同的包下,也就是不同的路径下。

那我就知道了,是 xml 文件扫描出问题了,肯定是 MyBatis 配置的 mapperLocations 有问题了,有可能是被我或者其他同事不小心多敲了个字母之类的。于是打开配置文件看了一下,

mybatis:
mapperLocations: com/xxx/aaa/mapper/*.xml,com/xxx/aaa/bbb/mapper/*.xml,com/xxx/aaa/ccc/mapper/*.xml

MyBatis 配置 mapperLocations 配置了三个包路径,也就是从这三个包中寻找 *.xml去解析,但是经过检查发现,并没有问题,配置文件没有 git 提交记录,而且配置的包路径也是正确无误的,其他两个包都扫描正常,就是 com/xxx/aaa/ccc/mapper/*.xml这个包有问题。于是我又试了如下几个方法:

  1. 把这个有问题的包路径放到第一个,无效。
  2. 把其他两个注释,只留这个有问题的,无效。
  3. 难道是 MyBatis 读取了其他地方的配置?于是我把这个配置注释掉,结果都出问题了,说明就是读的这个配置。

源码大法好

此时,已经过去很长时间了,问题变的越来越诡异,但是事出必有因,肯定是某些地方出现了问题。实在找不出项目本身的问题了,没办法,我只能怀疑是 MyBatis 有问题了,也许真的是触发了 MyBatis 本身的隐藏 bug。

不到万不得已是不会用这种方式的,那就是调试 MyBatis 源码。想来,MyBatis 源码我还是比较熟悉的。那咱们就再会一会吧。

mybatis-spring-boot-starter 只有三个 Java 文件,其中 MybatisAutoConfiguration是关键业务类。

而我们知道 MyBatis 中 SqlSessionFactory 是非常核心的对象,所以我们就把断点加在 sqlSessionFactory(DataSource dataSource)这个方法上。

如果是第一次调试开源框架源码,往往不能一下子找准位置,其实没有关系,把断点打在任何一个位置都可以,大不了就慢慢跟两遍嘛,本身读源码、调试的过程就是个漫长的过程。

@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 省略...
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}

以上代码我只保留了本次问题相关的代码,那就是解析 mapperLocations 的过程,也就是上面代码中this.properties.resolveMapperLocations()这个方法。

public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<Resource> resources = new ArrayList<Resource>();
if (this.mapperLocations != null) {
for (String mapperLocation : this.mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}

当我继续跟踪代码的时候,发现 MyBatis 确实已经识别到了配置文件中的那三个包路径,this.mapperLocations就是那三个包路径的数组集合。

接着往下跟,在方法 resourceResolver.getResources(mapperLocation)中对每一个路径进行解析,发现前两个包都正常返回了Resource[],也就是对应的 xml 文件资源,而最后一个返回的确实空数组,问题原因已经很近了。

接着再次启动调试,当解析最后一个包路径是,进入resourceResolver.getResources(mapperLocation)方法内部,看看里面都干了什么,最后发现在调用以下代码之后,返回的 rootDirURL 是一个绝对路径,也就是 xml 所在的物理路径。

URL rootDirURL = rootDirResource.getURL();

这时,终于发现问题所在了,这个绝对路径竟然不是 xml 所在的路径,而是另外一个子模块下的路径,经过对比发现,原来,子模块中被新建了一个名称一样的文件夹,造成存在两个完全一样的包路径,而以上代码返回了另一个包的绝对路径。于是,联系同事,问清楚这个包被创建的原因,发现是最近新加的但是已经废弃无用的,于是删掉解决了问题。

正常项目开发中应该可以规避这种问题,模块与模块不应该出现相同包名,应该遵循如下命名:

模块A:com.kite.moduleA

模块B: com.kite.moduleB

这样从根本上解决问题,以防出现不必要的麻烦。

最后

MyBatis 的这个异常确实令人头疼,因为错误原因不明显,以此类推,凡是 xml 文件造成的问题都不太容易排查,大部分情况都是人为疏忽造成的,而错误一般都比较隐蔽。

当一个问题经过多方验证都没办法被发现被解决的时候,往往就需要换个思路了,及时跳出来,从其它角度或者更高层次重新审视问题,也许能更快的找到问题原因。

在用开源框架的时候,如果出现问题,长时间找不到解决办法,那么可以尝试调试一下源码,并没有想象的那么困难。

壮士且慢,先给点个赞吧,总是被白嫖,身体吃不消!

我是风筝,公众号「古时的风筝」,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的很 6 的斜杠开发者。可以在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同学也在群内呦。

一个排查了大半天儿的问题,差点又让 MyBatis 背锅的更多相关文章

  1. 一个神秘URL酿大祸,差点让我背锅!

    神秘URL 我叫小风,是Windows帝国一个普通的上班族.上一回说到因为一个跨域请求,我差点丢了饭碗,好在有惊无险,我的职场历险记还在继续. "叮叮叮叮~~~~",闹钟又把我给吵 ...

  2. 《.NET 5.0 背锅案》第4集:一个.NET,两手准备,一个issue,加倍关注

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  3. Mac下GoogleChromeHelper占用内存过高 的一个排查过程记录

    测试需要在Mac上装了个虚拟机,结果忘记限制资源了,直接崩溃重启过一次. 后面限制了一下资源,发现内存占用率还是特别高,其中最高的居然是Chrome相关的一个东西.这让我8G内存该如何是好. 于是查了 ...

  4. Nginx 转发时的一个坑,运维居然让我背锅!!

    最近遇到一个 Nginx 转发的坑,一个请求转发到 Tomcat 时发现有几个 http header 始终获取不到,导致线上出现 bug,运维说不是他的问题,这个锅我背了. 新增的几个 header ...

  5. 阿里云SLB出现502 Bad Gateway 错误排查解决方法

    502 Bad Gateway The proxy server received an invalid response from an upstream server. 原本系统是通过一个SLB转 ...

  6. 如何写出一个让人很难发现的bug?

    程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因为我的眼里常含bug. 那么如何写出一个让(坑)人(王)很(之)难(王)发现的bug呢? - 1 -新手开发+ ...

  7. WEB网络问题的排查【转】

    Browser/Server结构主要是利用了不断成熟的Web浏览器技术:结合浏览器的多种脚本语言和ActiveX技术,用通用浏览器实现原来需要复杂专用软件才能实现的强大功能,同时节约了开发成本.B/S ...

  8. PostgreSQL的.NET驱动程序Npgsql中参数对象的一个Bug

    最近将公司的项目从SqlServer移植到PostgreSQL数据库上来,在调用数据库的存储过程(自定义函数)的时候,发现一个奇怪的问题,老是报函数无法找到. 先看一个PgSQL存储过程: CREAT ...

  9. 浅谈如何写出一个让(坑)人(王)很(之)难(王)发现的bug

    该文章内容来自脚本之家,原文链接:https://www.jb51.net/news/598404.html 程序员的日常三件事:写bug.改bug.背锅.连程序员都自我调侃道,为什么每天都在加班?因 ...

随机推荐

  1. 模糊字符串匹配:FuzzyWuzzy

    FuzzyWuzzy 模糊字符串匹配,它使用Levenshtein Distance来计算简单易用的包中序列之间的差异. 前置条件 Python 2.7 or higher difflib pytho ...

  2. centos7与8的区别

    1.关于内核版本:RHEL8采用4.18.0-xRHEL7采用3.10-0-x 2 网络时间同步 RHEL8 只使用Chronyd,不支持NTP部署. RHEL7Chronyd与NTP两者都支持 3. ...

  3. docker企业级镜像仓库Harbor管理

    Harbor概述 Harbor是由VMWare公司开源的容器镜像仓库.事实上,Harbor是在Docker Registry上进行了相应的企业级扩展,从而获得了更加广泛的应用,这些新的企业级特性包括: ...

  4. Python 如何写 Ubuntu syslog

    address='/dev/log' 是关键 import logging from logging.handlers import SysLogHandler logger = logging.ge ...

  5. Linux系统管理第三次作业 账号管理 权限及归属管理

    1.创建/guanli 目录,在/guanli下创建zonghe 和 jishu 两个目录(一条命令) [root@localhost ~]# mkdir /guanli [root@localhos ...

  6. OOM的起点到终点

    前言 1.问题及现象 线上日志反馈内存溢出问题.根据用户反馈,客户操作一段时间之后,APP 内存溢出崩溃. 2.分析过程 (1) 分析线上日志,发现主要分两种: 第一种如下,可能是某个死循环导致内存不 ...

  7. linux服务器(CentOS)一键安装express框架

    express框架需要nodejs环境支持,没有安装node.js环境的同学可以参照下面这篇博客 linux服务器安装配置Node.js 好了,言归正传.先使用xshell或者其它软件连接我们的服务器 ...

  8. Codeforce 1102 C. Doors Breaking and Repairing

    Descirbe You are policeman and you are playing a game with Slavik. The game is turn-based and each t ...

  9. dij-spfa乱搞

    以前见过一篇另类堆优化dij的题解,然而找不到了 那位作者称它为dij-spfa(大概是这个意思,然而确实很形象 这方法比较玄学,正确性没有严格证出来,然而对拍是验证猜想的最好途径 不过也可能并不玄学 ...

  10. App 自动化环境搭建

    1.安装 Appium-desktop 工具 下载地址:https://github.com/appium/appium-desktop/releases 2.安装 Android 环境 安装 JDK ...