昨天在新建Springboot启动后,发现执行相关的SQL报错 org.apache.ibatis.binding.BindingException: Invalid bound statement,具体报错信息如下:
1 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.hqtc.live.admin.common.dao.UserDao.list 2 at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:227) 3 at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:49) 4 at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:65) 5 at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58) 6 at com.sun.proxy.$Proxy89.list(Unknown Source) 7 at com.hqtc.live.admin.common.service.impl.UserServiceImpl.list(UserServiceImpl.java:47) 8 at com.hqtc.live.admin.common.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$eed42660.invoke(<generated>) 9 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) 10 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) 11 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) 12 at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99) 13 at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282) 14 at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) 15 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) 16 at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) 17 at com.hqtc.live.admin.common.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$137fcb94.list(<generated>) 18 at com.hqtc.live.admin.common.shiro.UserRealm.doGetAuthenticationInfo(UserRealm.java:46) 19 at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568) 20 at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180) 21 at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267) 22 at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198) 23 at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106) 24 at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270) 25 at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256) 26 at org.apache.shiro.web.filter.authc.AuthenticatingFilter.executeLogin(AuthenticatingFilter.java:53) 27 at org.apache.shiro.web.filter.authc.FormAuthenticationFilter.onAccessDenied(FormAuthenticationFilter.java:154) 28 at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133) 29 at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162) 30 at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203) 31 at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178) 32 at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131) 33 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) 34 at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66) 35 at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449) 36 at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365) 37 at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90) 38 at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83) 39 at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383) 40 at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362) 41 at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) 42 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) 43 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) 44 at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) 45 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) 46 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) 47 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) 48 at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:108) 49 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) 50 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) 51 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) 52 at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81) 53 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) 54 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) 55 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) 56 at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) 57 at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) 58 at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) 59 at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) 60 at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) 61 at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) 62 at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478) 63 at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) 64 at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) 65 at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) 66 at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) 67 at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) 68 at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) 69 at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) 70 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459) 71 at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) 72 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) 73 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) 74 at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) 75 at java.lang.Thread.run(Thread.java:745)
我在网上找了好多相关的blog,好多说是
1:Mapper.xml中的namespace不对应和mapper接口不对应
2:Mapper.xml中的方法(即id)和mapper接口中的方法名字不同或对应的方法不存在
3:返回类型不匹配(即没有正确配置ResultMap或者ResultType)
4:可能xml文件有缓存或者修改后没保存
5:可能没有配置MapperScan导致dao方法没有被扫描注入
6:配置文件中mybatis.mapper-locations或mybatis.typeAliasesPackage配置不正确
我逐条对比测试,发现不是上述的这些问题,那么问题出在哪里呢?
通过报错信息的第二行,我找到了MapperMethod.SqlCommand方法,具体代码如下:
1 public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { 2 final String methodName = method.getName(); 3 final Class<?> declaringClass = method.getDeclaringClass(); 4 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, 5 configuration); 6 if (ms == null) { 7 if (method.getAnnotation(Flush.class) != null) { 8 name = null; 9 type = SqlCommandType.FLUSH; 10 } else { 11 throw new BindingException("Invalid bound statement (not found): " 12 + mapperInterface.getName() + "." + methodName); 13 } 14 } else { 15 name = ms.getId(); 16 type = ms.getSqlCommandType(); 17 if (type == SqlCommandType.UNKNOWN) { 18 throw new BindingException("Unknown execution method for: " + name); 19 } 20 } 21 }
通过分析我们得出进入了ms==null的逻辑,因此我们分析MapperMethod.resolveMappedStatement这个方法,看为什么这个sql对应的MappedStatement为什么为空,分析其代码如下
1 private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, 2 Class<?> declaringClass, Configuration configuration) { 3 String statementId = mapperInterface.getName() + "." + methodName; 4 if (configuration.hasStatement(statementId)) { 5 return configuration.getMappedStatement(statementId); 6 } else if (mapperInterface.equals(declaringClass)) { 7 return null; 8 } 9 for (Class<?> superInterface : mapperInterface.getInterfaces()) { 10 if (declaringClass.isAssignableFrom(superInterface)) { 11 MappedStatement ms = resolveMappedStatement(superInterface, methodName, 12 declaringClass, configuration); 13 if (ms != null) { 14 return ms; 15 } 16 } 17 } 18 return null; 19 } 20 }
其中configuration.hasStatement(statementId)会判断有没有这个sql对应的方法。分析发现,org.apache.ibatis.session.Configuration方法中的属性 mappedStatements 为空。这就不难解释为什么报找不到方法的错误了。
那么为什么mappedStatements为空呢,我怀疑是mybatis.mapper-locations属性没有被正确加载解析,导致无法正确的找到每个接口方法对应的sql,那么接下来分析mybatis.mapper-locations有没有被加载。Mybatis相关的配置信息被加载后都存在MybatisProperties类中,通过调试查看这个类的属性发现,mybatis.mapper-locations已经被加载了。那么接下来看有没有被解析。我们找到了MybatisAutoConfiguration.sqlSessionFactory方法,具体代码如下:
1 @Bean 2 @ConditionalOnMissingBean 3 public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { 4 SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); 5 factory.setDataSource(dataSource); 6 factory.setVfs(SpringBootVFS.class); 7 if (StringUtils.hasText(this.properties.getConfigLocation())) { 8 factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); 9 } 10 Configuration configuration = this.properties.getConfiguration(); 11 if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { 12 configuration = new Configuration(); 13 } 14 if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { 15 for (ConfigurationCustomizer customizer : this.configurationCustomizers) { 16 customizer.customize(configuration); 17 } 18 } 19 factory.setConfiguration(configuration); 20 if (this.properties.getConfigurationProperties() != null) { 21 factory.setConfigurationProperties(this.properties.getConfigurationProperties()); 22 } 23 if (!ObjectUtils.isEmpty(this.interceptors)) { 24 factory.setPlugins(this.interceptors); 25 } 26 if (this.databaseIdProvider != null) { 27 factory.setDatabaseIdProvider(this.databaseIdProvider); 28 } 29 if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { 30 factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); 31 } 32 if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { 33 factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); 34 } 35 if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { 36 factory.setMapperLocations(this.properties.resolveMapperLocations()); 37 } 38 39 return factory.getObject(); 40 }
在最后你是不是看到了
1 if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { 2 factory.setMapperLocations(this.properties.resolveMapperLocations()); 3 }
按道理来说,这个方法是被@Bean注解修饰的,应该会被Spring自动注入,一单被自动注入,那么相应的会执行这个方法。但是通过调试发现,这个方法没有被执行,难怪mybatis.mapper-locations属性没有被正确解析。我突然想到,我们当时在做mysql主从的时候,在相关的配置类中自定义过 SqlSessionFactory 的实现和注入,有可能是这个问题。那么我尝试将我们自定义的SqlSessionFactory的实现给注释掉,重启后发现问题解决了。我们看下 MybatisAutoConfiguration 中关于 sqlSessionFactory 的注入代码部分如下:
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
我们看到了 @ConditionalOnMissingBean 这个注解,它的意思是如果BeanFactory中存在了 SqlSessionFactory,就不再注入当前的 SqlSessionFactory。这也就是我们自定义SqlSessionFactory的注入方法后,就不在执行这个注入代码的原因。
最后发现其实问题很简单,就是我们自定义的SqlSessionFactory 的注入覆盖了mybatis内部的注入方法,导致mybatis.mapper-locations没有被解析生效。
最后总结:
1:重视报错信息,通过报错信息我们往往能分析到问题产生的直接原因。
2:找到问题间的关联。就像上述问题,如果你熟悉mybatis的源码,你可能会很快的找到根本原因。如果你不熟悉源码,可以通过断点调试等方法,找到相关类和方法间的引用关系从而找到问题的关键。
3:还是多看看mybatis的源码吧。