本文只是一个排查过程的梳理总结,不会揭示一些构架原理
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相关代码。