问题描述
在进行一次项目结构改造(其实也就是搬迁了部分代码位置)时出现如下问题:
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.vilce.springboot_vue.module.article.service.JotterArticleService.countArticle
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:235) ~[mybatis-3.5.1.jar:3.5.1]
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:53) ~[mybatis-3.5.1.jar:3.5.1]
at org.apache.ibatis.binding.MapperProxy.lambda$cachedMapperMethod$0(MapperProxy.java:62) ~[mybatis-3.5.1.jar:3.5.1]
at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660) ~[na:1.8.0_211]
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:62) ~[mybatis-3.5.1.jar:3.5.1]
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:57) ~[mybatis-3.5.1.jar:3.5.1]
at com.sun.proxy.$Proxy88.countArticle(Unknown Source) ~[na:na]
...省略
简单来说就是mybaits
部分代码出现非法绑定错误,下面进行分析。
问题分析
首先可以确定的是代码没有问题,由于只是代码迁移,所以这里一定是由于代码路径的变动导致的问题。
代码迁移后关于mybatis
对应修改了两个部分:
MapperScan
扫描的包路径mapper-locations
指定的xml
文件路径
根据抛错的提示第二行:
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:235)
进入源码查看:
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);
}
}
}
进行debug
调试,发现ms
返回为空,进入resolveMappedStatement
:
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
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;
}
这里可以发现statementId
的值为com.vilce.springboot_vue.module.article.service.JotterArticleService.countArticle
,但是configuration
中statement
存储的是mapper-locations
指定路径下xml
中指定的mapper
,明显不对等。
所以问题一定是MapperScan
,这里需要了解MapperScan
的作用。
MapperScan
注解被MapperScannerRegistrar
的registerBeanDefinitions
方法所引用,目的是将backPages
定义的所有包下的所有接口生成一个org.apache.ibatis.binding.MapperProxy
代理bean
。
所以这里MapperScan
将service
接口也生成了代理,并且去匹配查找对应的Mapper
文件所以报错了。
问题解决
既然已经知道是MapperScan
在扫描生成代理bean
的时候将service
接口也扫描了进去,那么问题解决也很简单,有三种:
- 简单粗暴,修改
MapperScan
的扫描路径
// @MapperScan(basePackages = "com.vilce.module")
@MapperScan(basePackages = "com.vilce.module.mapper")
- 为
MapperScan
添加annotationClass=Mapper.class
,然后在需要的Mapper
类上加上@Mapper
注解
@MapperScan(basePackages = "com.vilce.module", annotationClass= Mapper.class)
- 直接使用
@Mapper
注解
// @MapperScan(basePackages = "com.vilce.module")
需要注意的是,使用方法二其实和方法三重复了,但是我们使用方法二的时候可以进行修改不使用Mapper.class
,这里也不能使用自定义注解,会报错无法找到对应的configuration
。例如我们的ArticleMapper
:
@Component
public interface ArticleMapper {
....
}
这里可以指定为Component
,但是注意,其他使用该注解的接口依旧会被扫描进来并生成代理类,所以这里不推荐方法二。
方法三最简单,将@Mapper
替代上面的@Component
即可。当然,使用@MapperScan
也有它的好处。这里需要根据自己的实际情况进行调整。