MyBatis:对不起,这锅我不能背(给年轻的程序员上一课呀)

本文分享了一次MyBatis异常排查经历,作者在增加功能模块时遇到invalidboundstatement异常,通过深入源码最终定位问题为同名包路径冲突导致。文章强调了问题排查时跳出固定思维的重要性,以及在开源框架遇到难题时调试源码的有效性。

前文

写代码多年,我一直有个习惯,只要是要做的功能模块不是很复杂,一般都是上来狂写一通代码,等功能做好了,再启动服务测试,哪里有问题再改(实话说,单元测试写的也不多)。

而不是写完一个接口或方法就测试一下,最长的记录应该是连着写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:对不起,这锅我不能背(给年轻的程序员上一课呀)

 

而我们知道 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 目录(?)[-] mybatis实战教程mybatis in action之开发环境搭建 mybatis实战教程mybatis in action之二以接口的方式编程 mybatis实战教程mybatis in action之三实现数据的增删改查 mybatis实战教程mybatis in action之四实现关联数据的查询 mybatis实战教程mybatis in action之五与spring3集成附源码 mybatis实战教程mybatis in action之六与Spring MVC 的集成 mybatis实战教程mybatis in action之七实现mybatis分页源码下载 mybatis实战教程mybatis in action之八mybatis 动态sql语句 mybatis实战教程mybatis in action之九mybatis 代码生成工具的使用 mybatis SqlSessionDaoSupport的使用附代码下载 转自:http://www.yihaomen.com/article/java/302.htm (读者注:其实这个应该叫做很基础的入门下下,如果你看过Hibernate了那这个就非常的简单) (再加条,其实大家可以看官方的教程更好些:http://mybatis.github.io/mybatis-3/,而且如果英文不是很好的那就看中文的:http://mybatis.github.io/mybatis-3/zh/sqlmap-xml.html) 写在这个系列前面的话: 以前曾经用过ibatis,这是mybatis的前身,当时在做项目时,感觉很不错,比hibernate灵活。性能也比hibernate好。而且也比较轻量级,因为当时在项目中,没来的及做很很多笔记。后来项目结束了,我也没写总结文档。已经过去好久了。但最近突然又对这个ORM 工具感兴趣。因为接下来自己的项目中很有可能采用这个ORM工具。所以在此重新温习了mybatis, 因此就有了这个系列的 mybatis 教程. 什么是mybatis MyBatis是支持普通SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plan Old Java Objects,普通的Java对象)映射成数据库中的记录. orm工具的基本思想 无论是用过的hibernate,mybatis,你都可以法相他们有一个共同点: 1. 从配置文件(通常是XML配置文件)得到 sessionfactory. 2. 由sessionfactory 产生 session 3. 在session 中完成对数据的增删改查和事务提交等. 4. 在用完之后关闭session 。 5. 在java 对象和 数据库之间有做mapping 的配置文件,也通常是xml 文件mybatis实战教程(mybatis in action):开发环境搭建 mybatis 的开发环境搭建,选择: eclipse j2ee 版本,mysql 5.1 ,jdk 1.7,mybatis3.2.0.jar包。这些软件工具均可以到各自的官方网站上下载。 首先建立一个名字为 MyBaits 的 dynamic web project 1. 现阶段,你可以直接建立java 工程,但般都是开发web项目,这个系列教程最后也是web的,所以开始就建立web工程。 2. 将 mybatis-3.2.0-SNAPSHOT.jar,mysql-connector-java-5.1.22-bin.jar 拷贝到 web工程的lib目录. 3. 创建mysql 测试数据库和用户表,注意,这里采用的是 utf-8 编码 创建用户表,并插入条测试数据 程序代码 程序代码 Create TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userName` varchar(50) DEFAULT NULL, `userAge` int(11) DEFAULT NULL, `userAddress` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; Insert INTO `user` VALUES ('1', 'summer', '100', 'shanghai,pudong'
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值