问题描述
在进行一次项目结构改造(其实也就是搬迁了部分代码位置)时出现如下问题:
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也有它的好处。这里需要根据自己的实际情况进行调整。