mybatis使用@SelectProvider构建动态语句,多个参数(不使用@param注解情况下),3.4.2版本之后报错 BindingException: Parameter 'arg0'

最近代码升级mybatis了的版本3.4.6,启动后发现 使用@SelectProvider构建多个参数(不使用@param注解情况下)的动态语句,在查询时会报错。
 org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.
官方例子

为此特意翻阅了mybatis的官方说明,找到下面的例子

@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List<User> getUsersByName(
    @Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

  // If not use @Param, you should be define same arguments with mapper method
  public static String buildGetUsersByName(
      final String name, final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }

  // If use @Param, you can define only arguments to be used
  public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }
}

经过测试,官方代码中编写的第一种写法(dao层的 buildGetUserByName 方法在每个参数前加上 @Param注解,内部类对应的 buildGetUserByName 方法则只需要把参数名、数量与dao层上的@Param注解保持一致;),在3.4.2版本前是正常可用的,但到了3.4.2版本后就会报错。

 org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.

从异常可以看出在没有使用@param注解的情况下,传递参数需要使用#{arg0}-#{argn}或者#{param1}-#{paramn}

报错原因

在项目启动的过程中,mybatis会查找所有的@SelectProvider标签,根据对应的type属性找到内部类,构建ProviderSqlSource对象,其中关键点在 providerMethodArgumentNames (方法参数名数组)属性的赋值。

3.4.2版本前的赋值方式
this.providerMethodArgumentNames = extractProviderMethodArgumentNames(m);

参数m代表当前的方法method,如官方例子中的 buildGetUserByName ;具体函数的实现:

  private String[] extractProviderMethodArgumentNames(Method providerMethod) {
    String[] argumentNames = new String[providerMethod.getParameterTypes().length];
    for (int i = 0; i < argumentNames.length; i++) {
      Param param = findParamAnnotation(providerMethod, i);
      argumentNames[i] = param != null ? param.value() : "param" + (i + 1);
    }
    return argumentNames;
  }

  private Param findParamAnnotation(Method providerMethod, int parameterIndex) {
    final Object[] annotations = providerMethod.getParameterAnnotations()[parameterIndex];
    Param param = null;
    for (Object annotation : annotations) {
      if (annotation instanceof Param) {
        param = Param.class.cast(annotation);
        break;
      }
    }
    return param;
  }

根据代码可以知道是先查找@Param,有的情况下则把Param标签的值作为参数名,没有则用"param1"~"paramn"作为参数名。providerMethodArgumentNames的属性为"param1"~"paramn";

3.4.2版本后的赋值方式
this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();

参数configuration是mybatis的配置,参数m是method

  public ParamNameResolver(Configuration config, Method method) {
    final Class<?>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<Integer, String>();
    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 (config.isUseActualParamName()) {
          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);
  }

  private String getActualParamName(Method method, int paramIndex) {
    if (Jdk.parameterExists) {
      return ParamNameUtil.getParamNames(method).get(paramIndex);
    }
    return null;
  }

由于没有使用@Param注解,代码会判断系统是否开启了 useActualParamName(使用java8的反射得到方法参数名),3.4.2版本后此属性默认为true,代码会执行ParamNameUtil.getParamNames()

public class ParamNameUtil {
  private static List<String> getParameterNames(Executable executable) {
    final List<String> names = new ArrayList<String>();
    final Parameter[] params = executable.getParameters();
    for (Parameter param : params) {
      names.add(param.getName());
    }
    return names;
  }

}


因为代理的原因,这里获取到的名字就是"agr0"~"agrn",而不是实际代码编写的参数名;providerMethodArgumentNames的属性为"agr0"~"agrn";

假如把useActualParamName设置为false,获取到的参数名则为"0"~"n";providerMethodArgumentNames的属性为"0"~"n";

项目启动完毕后,当执行代码查询时,mybatis会执行下列代码

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class<?>) {
        this.returnType = (Class<?>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

关键在于最后的 

this.paramNameResolver = new ParamNameResolver(configuration, method);

这里的实现跟之前构建ProviderSqlSource时给providerMethodArgumentNames赋值是一样的,区别时这里获取的是mapper里面的方法和参数,由于mapper方法上有写@Param注解,这里就能正确获取到参数的名称,并赋值到paramNameResolver 中。

当执行execute()方法前,mybatis会把mapper方法的参数名数组和参数值转换扩充为map。

    Object param = method.convertArgsToSqlCommandParam(args);

args就是参数值的数组

method.converArgsToSqlCommandParam()方法执行的是下面的逻辑

    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }
 /**
   * <p>
   * A single non-special parameter is returned without a name.<br />
   * Multiple parameters are named using the naming rule.<br />
   * In addition to the default names, this method also adds the generic names (param1, param2,
   * ...).
   * </p>
   */
  public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<Object>();
      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 + String.valueOf(i + 1);
        // ensure not to overwrite parameter named with @Param
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }
得到的map为["param1":"0","name":"123","param2":"","orderByColumn":""];

最终mybatis会执行下面的代码完成mapper中方法和SelectProvider类方法的参数对应

  private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
    Object[] args = new Object[argumentNames.length];
    for (int i = 0; i < args.length; i++) {
      if (providerContextIndex != null && providerContextIndex == i) {
        args[i] = providerContext;
      } else {
        args[i] = params.get(argumentNames[i]);
      }
    }
    return args;
  }


其中参数params就是我们上面获得到的mapper的map--["param1":"0","name":"123","param2":"","orderByColumn":""];参数argumentNames则为前面SelectProvider中的providerMethodArgumentNames,在3.4.2版本前为"param1"~"paramn",在3.4.2版本后为"arg0"~"argn"或"0"~"1";所以程序在3.4.2版本前可以正常跑,在在3.4.2版本后则报

org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.
org.apache.ibatis.binding.BindingException: Parameter '0' not found.


解决办法

解决的办法很简单,在SelectProvider方法的参数名前对应加上与mapper方法一样的@Param注解。


文章参考了 等风de帆- 29、ParamNameResolver参数解析 和 彼岸的包子 的mybatis传多个参数(不使用@param注解情况下),3.4.2版本之后使用#{0}-#{n}引起的参数绑定异常,以及settings属性中useActualParamName的作用。


  • 13
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值