一个排查了大半天儿的问题,差点又让 MyBatis 背锅

我是风筝,公众号「古时的风筝」,一个不仅有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。
Spring Cloud 系列文章已经完成,能够到 个人github 上查看系列完整内容。也能够在公众号内回复「pdf」获取我精心制做的 pdf 版完整教程。java

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

代码路上的滑铁卢

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

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

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

出现这个异常多是下面的这几个缘由:数据库

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

若是你是用工具自动生成 xml 还好,若是是手动建立的,那极可能因为疏忽出现这个问题,好比咱们从另外一个文件复制过来,忘记改 namespace 了,或者接口方法名和 statement id 差了一个字母或者字母顺序不一致。这个异常是很使人头疼的,就好比相差一个字母这种状况,很难被发现,因此最好仍是写好接口方法名,而后复制到 xml 中。数组

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

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

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

有时候问题很奇怪,可能和 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 的斜杠开发者。能够在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同窗也在群内呦。

相关文章
相关标签/搜索