36

MyBatis:这锅我不背

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAxMjA0MDk2OA%3D%3D&%3Bmid=2449469561&%3Bidx=1&%3Bsn=ae29cf019416d17ddc8d8165a36a2d02
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

6RrA3iu.png!web

古时的风筝第 72  篇原创文章 

写代码多年,我一直有个习惯,只要是要做的功能模块不是很复杂,一般都是上来狂写一通代码,等功能做好了,再启动服务测试,哪里有问题再改(实话说,单元测试写的也不多)。而不是写完一个接口或方法就测试一下,最长的记录应该是连着写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 工作原理分析

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

ZnEfuqr.png!web

而我们知道 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 文件造成的问题都不太容易排查,大部分情况都是人为疏忽造成的,而错误一般都比较隐蔽。

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

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

壮士且慢,想着点个在看,或者转发个朋友圈也行啊

还可以读

先从简单的源码入手:MyBatis 工作原理分析

-----------------------------------------

公众号:古时的风筝 , 一个不只有技术的技术公众号。

我是风筝,一个主业 Java,同时也擅长 Python、React 的斜杠开发者。你可选择现在就关注我,或者看看历史文章再关注也不迟。

技术交流还可以加群或者直接加我微信。

zYVzAjq.jpg!web

【太阳出来了】

BBR7riA.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK