MyBatis 版本:3.5.6
Java 8
背景
开局一张图……
en~,相信大家都碰到过上面图片中 MyBatis 的问题 ,在我传递多个参数的时候,我使用形参去 xml 中获取参数就会报错,而这个报错又是飘忽不定的,好像有时候不会错,但有时候又不行。
如果是熟悉 MyBatis 的老手,那么看到打印出的报错:
- 就会直到说把你 XML 文件获取参数依次换成 0, 1 或者 param1, param2。
- 再者就是直接使用 @Param("xx") 注解指定一个名称,XML 就不用变了。
是的,我看过源码之后,我对上面的说法完全理解,并且强力建议使用 @Param 注解的方式传入多个参数。
可能有的朋友还碰到过 arg0、arg1情况,后面咱们接着说。
debug
首先是你得搭建好一个 Spring+MyBatis 的项目,并提供一个多参数的查询接口。如果说我们不熟悉 MyBatis ,不知道如何去调式它的源码。也不用慌,打印的报错信息会给我们提示的。
org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:212)
上面就提示 最后的报错类是:MapperMethod,首先我们找到这个类的这个方法,打个断点,然后重新调用,当然有时候具体逻辑就在这个方法里面那就更好了,但是更多的时候不在这个方法里面,这里只是调用的时候具体的报错。这个时候只能耐心慢慢看调用栈了,从最开始我们业务代码开始看,
发现执行这个方法的时候好像有戏,涉及到 param 。而且,你瞅瞅这函数名:convertArgsToSqlCommandParam 把参数转为 SQL 参数,可不就是我们想要找的操作吗?赶紧断点打起来。
最终会定位到下面的源码。其中 GENERIC_NAME_PREFIX=“param” 在加上后面的 i+1 可不就是最开始报错打印的 param1、param2 吗。
public Object getNamedParams(Object[] args) {
final int paramCount = names.size();
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
Object value = args[names.firstKey()];
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
但是 0/1怎么回事呢?发现代码中还对 names 遍历取值设置。然后又是看看 names 在哪里被设置值的,然后断点再次打起来。重启项目,访问接口。最终定位到如下代码:下面代码就是真正的所有逻辑了。
public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
final Class<?>[] paramTypes = method.getParameterTypes();
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
// skip special parameters
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
if (name == null) {
// @Param was not specified.
if (useActualParamName) {
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// use the parameter index as the name ("0", "1", ...)
// gcode issue #71
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
names = Collections.unmodifiableSortedMap(map);
}
注解param
首先看参数的第一次判断就是判断是否有 Param 注解。有的话直接拿注解的值,然后跳过。所以这就是为什么强烈建议使用注解的方式,如果你用注解了,后面就不会有那么多乱七八糟的事情了。
属性useActualParamName
false
如果没用使用注解:会先判断下面这个属性,这个在 Configuration 的构造函数中设置为 true 。但是在配置文件中也能设置为 false。
useActualParamName
mybatis-plus:
mapper-locations: classpath:*Mapper.xml
configuration:
use-actual-param-name: false
设置为 false 的情况也是一目了然就是取 0、1、2...这样
true
基本上没人会在配置文件中配置这个属性,所以一定会走 name = getActualParamName(method, paramIndex); 这个方法。最终的代码时走下面的:
- tmp = getParameters0();是一个本地方法,直接调用 C++ 代码获取参数名
- synthesizeAllParams() 就是拼接 arg0、arg1 的逻辑,和 param0、param1 逻辑差不多
private Parameter[] privateGetParameters() {
// Use tmp to avoid multiple writes to a volatile.
Parameter[] tmp = parameters;
if (tmp == null) {
// Otherwise, go to the JVM to get them
try {
tmp = getParameters0();
} catch(IllegalArgumentException e) {
// Rethrow ClassFormatErrors
throw new MalformedParametersException("Invalid constant pool index");
}
// If we get back nothing, then synthesize parameters
if (tmp == null) {
hasRealParameterData = false;
tmp = synthesizeAllParams();
} else {
hasRealParameterData = true;
verifyParameters(tmp);
}
parameters = tmp;
}
return tmp;
}
那么调用 native 方法的条件是什么?参考 MyBatis 官网
mybatis.org/mybatis-3/z… useActualParamName
| useActualParamName | 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) | true | false 默认 true | | ------------------ | -------------------------------------------------------------------------------- | --------------------- |
如果是在 IDEA 中 -parameters 参数:
综上,native 方法才会获取到入参本来的参数名,即使你没有使用 @Param 注解。
总结
多入参,MyBatis 处理属性可能有四种情况:
- 0、1、2……
- arg0、arg1、arg2……
- param0、param1、param2……
- 保持原来的形式参数
第一种情况是:没有使用 Param 注解,且 useActualParamName 手动设置为 false。
第二种情况是:没有使用 Param 注解,useActualParamName 也为 true ,但是没有在 Java8 环境下使用 -parameters 参数编译。
第三种情况是:无论怎样都会出现。就是我多参数使用 param0 这样的,都不会出错
第四种情况:
- 使用 Param 注解
- 不使用 Param 注解,但是在 Java8 环境下使用 -parameters 参数编译。
——————————————————————
最后我们多参数的时候一定要加上注解,这样不易出错,而且参数名更有意义,容易检查。