从一个随机出现的错误开读Mybatis源码
关于Mapper方法重载
对于两个拥有相同方法名,但入参不同的方法,Mybatis会如何处理?
@Select("sql xxx")
Long statisticTotal(@Param("beginTime") String beginTime, @Param("endTime") String endTime);
@Select("sql xxx")
Long statisticTotal(@Param("name") String name);
- 如果采用传统的 XML,XML 必须指定一个id,当在启动时,就会报这个 id 冲突了,这个问题在之前遇到过。
- 但如果不用 XML,用的是注解呢? 思考这个问题是处于是想调用两个重载的方法中的第一个双参方法,却发现偶尔成功,偶尔报错: Paramter not found,没有发现这个报错的规律。
开读
入口
从一些地方了解到,Mybatis 其实就是在运行时,对于每个 Mapper 的方法调用最终都会被一个代理所捕获:
// 在Mybatis中是在org.apache.ibatis.binding.MapperProxy
// MyBatisPlus中是在com.baomidou.mybatisplus.core.override.MybatisMapperProxy
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
在 cachedInvoker 方法中,其会将接口的方法进行包装为 DefaultMethodInvoker 或者 PlainMethodInvoker:
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
其中 DefaultMethodInvoker 是为了实现默认方法的调用而实现的,重点还是 PlainMethodInvoker,其创建时又需要传递一个 MapperMethod,这个 MapperMethod 就是在 cachedInvoker 后 invoke 的东西,在 MyBatisPlus 中这个类是 MybatisMapperMethod。
方法转为命令表示
在 MapperMethod 创建时,会将接口的方法转为一条命令:
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
其内部就是根据 Mapper 接口名称以及方法在配置中寻找对应的 Statement,并且在 resolveMappedStatement 方法中,它生成了一个 statementId 就是用来寻找 Statement:
// MapperMethod
String statementId = mapperInterface.getName() + "." + methodName;
由此可见,参数并不构成这个唯一ID,至此 Command 的内容就差不多清楚了 而且这个 statementId 就是command 的name:
// MapperMethod.SqlCommand
name = ms.getId();
type = ms.getSqlCommandType();
命令执行
回到 MapperMethod 的 execute(MyBatisPlus中是invoke) 代理捕获调用后来到这里 根据先前的 command 的name 去调用sqlSession
而 sqlSession 则根据这个 name 去查找 MappedStatement,再根据 MappedStatement 以及传递过来的参数构造SQL语句,最后发到数据库去查询
// org.apache.ibatis.session.defaults.DefaultSqlSession
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
// org.apache.ibatis.executor.BaseExecutor
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
猜测
那么最后还有一个问题,就是两个同名的方法存在时,我去调用其中一个,却时好时坏,我的猜测是这个 MappedStatement 的加载顺序是不固定的,相同 id 的 Statement 会导致后面扫描到的覆盖之前的,但如果这样的话,结果应该是确定的,为什么有时报错有时却不报错?
验证
但是在刚才的调用链路中似乎并没有发现是如何加载 Statement 的,于是换个思路,从 SpringBoot 的自动装配来着手,在 MybatisPlus starter 的 jar 包下,有个 spring.factories 的文件 里面记录了要自动装配哪些类。
当然就发现了 MybatisPlusAutoConfiguration,里面有个内部类:AutoConfiguredMapperScannerRegistrar 看名字就是这个了
这个类声明一大堆对Mapper的描述为BeanDefinition 并交由 MapperScannerConfigurer 来进行扫描,经过一顿扫描,但在这里没怎么读懂把 Configuration 注入到 Spring 的,我猜大概是利用了 Spring 的一些机制,最后是会调用到:
configuration.addMapper(this.mapperInterface);
最后来到MapperRegistry (MyBatisMapperRegistry),这里会对每个Mapper接口的方法进行处理:
// addMapper
MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type);
parser.parse();
// 对Mapper接口的每个方法做处理
for (Method method : type.getMethods()) {
if (!canHaveStatement(method)) {
continue;
}
if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
&& method.getAnnotation(ResultMap.class) == null) {
parseResultMap(method);
}
try {
// TODO 加入 注解过滤缓存
InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method);
parseStatement(method);
} catch (IncompleteElementException e) {
// TODO 使用 MybatisMethodResolver 而不是 MethodResolver
configuration.addIncompleteMethod(new MybatisMethodResolver(this, method));
}
}
结论
重点就在于 type.getMethods 这个地方,随着我每次启动JVM,每次获取的 methods 返回的数组顺序是不一样的,这就导致我之前那个时好时坏的问题,后面的相同的 statementId 覆盖了之前的,但是谁先谁后并不是一个确定性的行为,Class.getMethods 的文档其实也说明了:
// The elements in the returned array are not sorted and are not in any particular order
// 返回的元素没有特定顺序
调试直到这里,也是印证了我的猜测,不过问题不出在MyBatis,而是出在JDK的反射机制上