zoukankan      html  css  js  c++  java
  • 一个排查了大半天儿的问题,差点又让 MyBatis 背锅

    我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 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 的斜杠开发者。可以在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同学也在群内呦。

  • 相关阅读:
    第二周学习总结
    第一周学习进度总结
    淘宝网质量属性分析
    软件架构师如何工作
    寒假学习记录第十六天
    寒假学习记录第十五天
    寒假学习记录第十四天
    寒假学习记录第十三天
    三.NFS存储服务
    二.Rsync备份服务
  • 原文地址:https://www.cnblogs.com/fengzheng/p/12907122.html
Copyright © 2011-2022 走看看