MyBatis中参数名称设置规则源码分析

MyBatis 是我们经常使用的 ORM 框架,在使用的过程中我最容易出现的问题就是参数没有传递正确,然后抛出异常说我们在 Mapper 文件中使用的一些参数没有找到。一开始遇到这种错误还挺懵的,后来遇到多了也能很容易的找出原因,特别是在表字段经常发生错误的情况下特别容易出现。但是对于 MyBatis 如何根据 Mapper 文件中引用的参数如何在接口中找到对应的值一直没有进行过研究,现在对 MyBatis 中参数的映射规则进行记录一下。

一般为了能让 Mapper 文件中的引用能正确找到接口中参数,我们会在接口中的参数上添加上 '@Param' 注解。

  • 接口
void deleteVoucherRecord(@Param("orderId") int orderId, @Param("voucherType") int voucherType);
  • Mapper文件
<update id="deleteVoucherRecord">
        UPDATE `voucher_record`
        SET `is_deleted` = 1
        WHERE
            `order_id` = #{orderId} AND `is_deleted` = 0 AND `voucher_category` =#{voucherType}
</update>

如果我们在接口中的参数列表中使用注解,那么 MyBatis 会根据 Mapper 文件中引用的参数名去接口中寻找相应参数的值。如果我们没有使用 '@Param' 注解的话 MyBatis 会使用一套另外一套规则,下面根据源码来了解这套规则。

因为我们写的接口最终会被代理,所以我们直接看 org.apache.ibatis.binding.MapperProxy 这个类,因为接口中的方法最终被调用时最终会在它的代理类中执行。

  @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 if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }

我们可以看倒数第二行代码,根据接口的 Method 找到对应的 MapperMethod,也就是根据接口中的 deleteVoucherRecord 方法找到 Mapper文件中 id 为 'deleteVoucherRecord' 的 SQL 语句。

下面来看看 MapperMethod 类中的 execute 方法:

  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional() &&
              (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName() 
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

通过上面的代码可以看到我们从接口中传入的参数经过转换返回了一个 Object,至于 Object 是什么我们现在无法知道,所以继续跟进代码进行推理与验证。

org.apache.ibatis.binding.MapperMethod.MethodSignature#convertArgsToSqlCommandParam 代码内容如下:

    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }

这段代码块并没有什么功能逻辑,它将该功能委托给了 org.apache.ibatis.reflection.ParamNameResolver 类来处理,这里可以看到单一职责的设计模式,就行领导不会做很底层的事情一样,领导只需要负责指挥、调控就行了,具体的活就交给小弟完成就行了。

org.apache.ibatis.reflection.ParamNameResolver#getNamedParams 代码内容如下:

  /**
   * <p>
   * A single non-special parameter is returned without a name.
   * Multiple parameters are named using the naming rule.
   * 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<>();
      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;
    }
  }

这是 MyBatis 参数映射逻辑关键代码,主要的逻辑都在这里。我们可以看到根据不同的条件返回值的类型也不一样。

  • 首先来分析一下接口中没有使用注解并且参数个数为一的情况,返回的值是 'args[names.firstKey]',这里 'names' 是一个成员变量,里面保存的是接口中函数的参数信息,它的类型是 SortedMap<Integer, String>,从这里可以看到这是一个排序的 Map,key 保存的是函数中参数的位置,value 保存的是函数中参数的名称(如果使用了注解那么 value 就是注解中的值,没有使用注解的话就是 arg0、arg1)。这里需要注意下的是我们其实无法通过反射拿到函数中参数的真是名称,拿到的参数名称是 arg0、arg1(跟JDK版本有关,可有可能是0、1) 这种形式,当然 JAVA8 可以通过在编译时添加 '-parameters' 参数可以获取到真实的参数名,MyBatis 对这个特性在 3.4.1 上面有说明,但是我没有进行验证,如果读者有兴趣可以去验证下结果然后留言告诉我结果😝。这里作者这样写的意图应该是如果只有一个参数知不知道参数的真实名称已经没有什么意义了,因为可以进行选择的参数也就一个不存在映射错乱的问题。

  • 如果参数不为空并且使用了注解的情况下返回的结果是 ParaMap ,这是 MyBatis 继承 HashMap 自定义的 Map,覆写了 get 方法。ParaMap 的 key 首先会保存通过注解或者反射回去到参数名,与此同时还会添加一个通用的参数名 'genericParamName',生成规则是 'GENERIC_NAME_PREFIX + String.valueOf(i + 1)',这里可以看到通用参数名的后缀是从1开始算的。

上述代码中 'names' 是一个比较重要的成员变量,它是在 ParamNameResolver 类的构造函数中进行初始化的,它的初始化与用户的一项配置 'useActualParamName' 有关,代码如下:

  public ParamNameResolver(Configuration config, Method method) {
    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 (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);
  }

如果我们配置 'useActualName' 为 true,那么 MyBatis 会尝试获取参数的真实名称,但最终是否能获取到跟具体的 JAVA Compiler 有关,所以如果不想使用 '@Param' 注解,这项配置也不能少。

发表评论

电子邮件地址不会被公开。 必填项已用*标注