MyBatis 多入参问题

574 阅读3分钟

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 这样的,都不会出错

第四种情况:

  1. 使用 Param 注解
  2. 不使用 Param 注解,但是在 Java8 环境下使用 -parameters 参数编译。

——————————————————————

最后我们多参数的时候一定要加上注解,这样不易出错,而且参数名更有意义,容易检查。