【记一次bug查询过程】invalid bound statement (not found)

本文只是一个排查过程的梳理总结,不会揭示一些构架原理

Exception in thread "main" org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.jd.ads.agent.dao.account.DspAgentDao.selectById
	at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:235)
	at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53)
	at org.apache.ibatis.binding.MapperProxy.lambda$cachedInvoker$0(MapperProxy.java:115)
	at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
	at org.apache.ibatis.binding.MapperProxy.cachedInvoker(MapperProxy.java:102)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:85)
	at com.sun.proxy.$Proxy13.selectById(Unknown Source)
	at com.Application.main(Application.java:17)

百度"Invalid bound statement (not found)"的结果:
  1:Mapper.xml中的namespace不对应和mapper接口不对应

2:Mapper.xml中的方法(即id)和mapper接口中的方法名字不同或对应的方法不存在

3:返回类型不匹配(即没有正确配置ResultMap或者ResultType)

4:可能xml文件有缓存或者修改后没保存

5:可能没有配置MapperScan导致dao方法没有被扫描注入

6:配置文件中mybatis.mapper-locations或mybatis.typeAliasesPackage配置不正确

逐一排查之后发现我并不属于上述情况,于是准备通过查看mybatis源码排查问题。
根据报错信息找到如下方法

    public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
          name = null;
          type = SqlCommandType.FLUSH;
        } else {
          throw new BindingException("Invalid bound statement (not found): "
              + mapperInterface.getName() + "." + methodName);// 此处抛异常
        }
      } else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }

对比一个运行正常的mybatis程序,发现此段代码中的局部变量ms不可能为空。
ms是resolveMappedStatement返回的,进入此方法

    private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      String statementId = mapperInterface.getName() + "." + methodName;
      if (configuration.hasStatement(statementId)) {// 异常程序此处为false
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }

发现在configuration.hasStatement(statementId)处正常程序和异常程序不同。进一步研究发现异常程序configuration中的mappedStatements为空。于是查询mappedStatements是哪儿来的。
mappedStatements是由Configuration.addMappedStatement进行添加的

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

调试时发现异常程序根本不会进入addMappedStatement方法中。
正常程序中addMappedStatement的调用堆栈如下

addMappedStatement:768, Configuration (org.apache.ibatis.session)
addMappedStatement:297, MapperBuilderAssistant (org.apache.ibatis.builder)
parseStatementNode:113, XMLStatementBuilder (org.apache.ibatis.builder.xml)
buildStatementFromContext:138, XMLMapperBuilder (org.apache.ibatis.builder.xml)
buildStatementFromContext:131, XMLMapperBuilder (org.apache.ibatis.builder.xml)
configurationElement:121, XMLMapperBuilder (org.apache.ibatis.builder.xml)
parse:95, XMLMapperBuilder (org.apache.ibatis.builder.xml)
buildSqlSessionFactory:520, SqlSessionFactoryBean (org.mybatis.spring)
afterPropertiesSet:381, SqlSessionFactoryBean (org.mybatis.spring)
invokeInitMethods:1687, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1624, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
doCreateBean:555, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
createBean:483, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
getObject:306, AbstractBeanFactory$1 (org.springframework.beans.factory.support)
getSingleton:230, DefaultSingletonBeanRegistry (org.springframework.beans.factory.support)
doGetBean:302, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:197, AbstractBeanFactory (org.springframework.beans.factory.support)
preInstantiateSingletons:742, DefaultListableBeanFactory (org.springframework.beans.factory.support)
finishBeanFactoryInitialization:867, AbstractApplicationContext (org.springframework.context.support)
refresh:543, AbstractApplicationContext (org.springframework.context.support)
loadContext:128, AbstractGenericContextLoader (org.springframework.test.context.support)
loadContext:60, AbstractGenericContextLoader (org.springframework.test.context.support)
delegateLoading:108, AbstractDelegatingSmartContextLoader (org.springframework.test.context.support)
loadContext:251, AbstractDelegatingSmartContextLoader (org.springframework.test.context.support)
loadContextInternal:98, DefaultCacheAwareContextLoaderDelegate (org.springframework.test.context.cache)
loadContext:116, DefaultCacheAwareContextLoaderDelegate (org.springframework.test.context.cache)
getApplicationContext:83, DefaultTestContext (org.springframework.test.context.support)
injectDependencies:117, DependencyInjectionTestExecutionListener (org.springframework.test.context.support)
prepareTestInstance:83, DependencyInjectionTestExecutionListener (org.springframework.test.context.support)
prepareTestInstance:230, TestContextManager (org.springframework.test.context)
springTestContextPrepareTestInstance:149, AbstractTestNGSpringContextTests (org.springframework.test.context.testng)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeMethod:134, MethodInvocationHelper (org.testng.internal)
invokeMethodConsideringTimeout:63, MethodInvocationHelper (org.testng.internal)
invokeConfigurationMethod:348, ConfigInvoker (org.testng.internal)
invokeConfigurations:302, ConfigInvoker (org.testng.internal)
invokeBeforeClassMethods:176, TestMethodWorker (org.testng.internal)
run:122, TestMethodWorker (org.testng.internal)
accept:-1, 1205483858 (org.testng.TestRunner$$Lambda$52)
forEach:1257, ArrayList (java.util)
privateRun:766, TestRunner (org.testng)
run:587, TestRunner (org.testng)
runTest:384, SuiteRunner (org.testng)
runSequentially:378, SuiteRunner (org.testng)
privateRun:337, SuiteRunner (org.testng)
run:286, SuiteRunner (org.testng)
runSuite:53, SuiteRunnerWorker (org.testng)
run:96, SuiteRunnerWorker (org.testng)
runSuitesSequentially:1187, TestNG (org.testng)
runSuitesLocally:1109, TestNG (org.testng)
runSuites:1039, TestNG (org.testng)
run:1007, TestNG (org.testng)
run:66, IDEARemoteTestNG (com.intellij.rt.testng)
main:109, RemoteTestNGStarter (com.intellij.rt.testng)

最后将问题进一步定位到buildSqlSessionFactory:520, SqlSessionFactoryBean (org.mybatis.spring)

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }
............省略......

异常程序中this.mapperLocations为空,而这个字段是spring的xml配置文件中注入的。然后发现此字段在xml中我配置了无效路径classpath*:agent/sqlmap/**/*Dao.xml,这个表达式匹配不到任何路径,所以mapperLocations为空,将其改成classpath*:agent.sqlmap/**/*Dao.xml后可以正常运行。

总结
虽然没有揭示什么运行原理,bug本身也是个弱智的bug,但是整个问题排查过程中收获了很多。
1、如果一个问题在网上找不到解决方案,可以尝试通过阅读源码的方式排查问题。这样既能可以进一步了解框架的运行方法,同时这种排查过程也是一种思维的锻炼,工作中也会用得上(公司内部代码产生的bug可能不会在外网上找到解决方法,此时要么问同事要么看代码)。但是也不是什么代码都读,有些代码比较底层就不要费时间去读。比如spring中对于xml文件解析的实现方法这部分我就没有读,整个排查问题的过程我只读了mybatis和springIOC相关代码。
2、对于一些常用代码要有一定的了解,这样可以在日后的工作学习中快速排查的问题。比如springIOC相关代码,我之前有一定的了解,所以在此次排查问题的过程中不需要特别仔细阅读springIOC相关代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值