窥探系列之Mybatis-plus 参数名解析

ParamNameResolver

当我们使用 MyBatis 进行数据库操作时,常常需要编写 SQL 语句,并且需要将方法的参数传递给 SQL 语句中的参数占位符。MyBatis 提供了一种方便的方式来实现这一点,即使用 #{paramName} 这种形式的参数占位符,并将方法的参数存储到一个 Map 对象中,然后将该 Map 对象传递给 SQL 语句执行器。但是,Java 的字节码中并没有对方法参数名进行记录,因此 MyBatis 需要一种方法来获取方法参数的名称,以便能够正确地将参数值传递给 SQL 语句。

为了解决这个问题,MyBatis 提供了一个名为 ParamNameResolver 的类,用于解析方法中的参数名。ParamNameResolver 使用 Java 的反射机制中的 Executable 接口提供的 getParameters() 方法获取方法的所有参数对象,然后结合字节码中的局部变量表信息,解析出方法的参数名。如果方法中使用了 @Param 注解,则会优先使用注解中的参数名作为方法参数的名称。

下面是 ParamNameResolver 的核心代码片段:

public class ParamNameResolver {
    public ParamNameResolver(Configuration config, Method method) {
        // ...
        final Class<?>[] paramTypes = method.getParameterTypes();
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        final List<String> names = new ArrayList<>();
        for (int i = 0; i < paramTypes.length; i++) {
            String name = null;
            // get the @Param annotation value
            for (Annotation annotation : paramAnnotations[i]) {
                if (annotation instanceof Param) {
                    name = ((Param) annotation).value();
                    break;
                }
            }
            if (name == null) {
                // use the parameter name as the default value
                name = getActualParamName(method, i);
            }
            names.add(name);
        }
        this.names = Collections.unmodifiableList(names);
    }

    private String getActualParamName(Method method, int paramIndex) {
        // use ASM to get the parameter name
        // ...
    }

    // ...
}

ParamNameResolver 的主要作用是为 MyBatis 提供一个统一的方式来获取方法参数的名称,从而能够正确地将参数值传递给 SQL 语句。在 MyBatis 中,如果 SQL 语句中的参数占位符使用了 #{paramName} 这种形式,那么 MyBatis 就会通过 ParamNameResolver 来获取对应的参数名,并从执行器传递的参数 Map 对象中获取对应的参数值,并将其绑定到 SQL 语句中的参数占位符上。

总之,ParamNameResolver 是 MyBatis 中的一个重要组件,它为 MyBatis 提供了一种方便的方式来获取方法参数的名称,从而使得 MyBatis 能够更加灵活、方便地实现 SQL 语句和 Java 方法之间的参数传递。

该代码是 MyBatis 中的 ParamNameResolver 类的构造函数实现。主要功能是解析方法参数的名称,并将参数名称与其索引映射到一个 SortedMap 中。

首先,构造函数会根据 MyBatis 的配置,判断是否使用实际参数名称。然后,获取方法的所有参数类型和注解,以及参数数量。接着,对于每个参数,通过遍历其注解,获取该参数的名称,如果没有找到 @Param 注解,就使用默认的名称。

如果启用了使用实际参数名称,且该参数没有指定名称,就会尝试从方法中获取该参数的实际名称。如果还是没有找到名称,就将参数的索引作为名称。

最后,构造函数将参数名称与索引映射关系保存在一个 SortedMap 中,并将其封装成一个不可修改的 SortedMap。

需要注意的是,在代码中,使用了 isSpecialParameter 方法来判断参数是否为特殊参数,这些特殊参数包括 RowBounds、ResultHandler 和 ParamMap 等类型,这些参数通常不需要指定名称。

在 for 循环中,使用 break 来提前结束循环,可以避免不必要的遍历,提高代码的效率。另外,由于 names 是一个不可修改的 SortedMap,因此可以使用 Collections.unmodifiableSortedMap 方法来创建一个不可修改的映射。

总的来说,该构造函数的作用是解析方法参数的名称,并将参数名称与其索引映射到一个 SortedMap 中。这个 SortedMap 可以方便地用于后续的参数解析操作,使得 MyBatis 可以更加灵活地处理方法参数。

  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);
  }

在 MyBatis 中,getNamedParams 方法用于获取方法参数列表中使用了 @Param 注解的参数名及其对应的参数值,以及未使用 @Param 注解的参数名及其对应的参数值。

如果参数数量为 0 或 args 数组为 null,则返回 null。如果参数数量为 1 且未使用 @Param 注解,则直接使用参数的值,将其转换为一个 Map 对象并返回。否则,将参数和名称的映射关系保存到一个 Map 中,并返回该 Map。

该代码首先创建一个 ParamMap 对象,然后遍历names和args,取出参数和名称的映射关系,将其添加到 Map 中。同时,还会使用一个计数器变量 i,用于生成使用默认命名规则的参数名称,这些参数名称以 “param” 开头,并以数字结尾,例如 “param1”、“param2” 等,然后添加到Map中。

在添加参数时,代码会先添加names中的参数名,然后再添加使用默认命名规则的参数,并避免覆盖已经具有名称的参数。

  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;
    }
  }

wrapToMapIfCollection用于将方法的参数列表中的集合或数组类型的参数包装成 Map 对象。

如果参数 object 是集合类型,则会创建一个 ParamMap 对象,并将集合对象存储在该 Map 对象中。同时,会添加一个名为 collection 的键,对应的值为集合对象。同时,如果actualParamName 不为空,则往ParamMap 对象中添加一个名为 actualParamName 的键,对应的值也为数组对象。

如果集合对象还是 List 类型,则会添加一个名为 list 的键,对应的值也为集合对象。

如果参数 object 是数组类型,则与集合类型类似,也会创建一个 ParamMap 对象,并会添加一个名为 array 的键,对应的值为数组对象。同时,如果actualParamName 不为空,则往ParamMap 对象中添加一个名为 actualParamName 的键,对应的值也为数组对象。

如果参数 object 不是集合或数组类型,则会直接返回 object,而不做任何处理。因为只有一个参数的话,将其包装成 Map 对象并没有太多实际意义,而直接返回对象更加简单和高效。

需要注意的是,ParamMap 是 MyBatis 中的一个特殊的 Map 实现,它可以用于存储多个同名的键值对。这样,我们就可以在 SQL 语句中使用 #{paramName} 占位符来引用参数,而不必担心参数名的重复问题。同时,ParamMap 还可以像普通的 Map 一样使用,支持添加、删除、遍历等操作。

  public static Object wrapToMapIfCollection(Object object, String actualParamName) {
    if (object instanceof Collection) {
      ParamMap<Object> map = new ParamMap<>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    } else if (object != null && object.getClass().isArray()) {
      ParamMap<Object> map = new ParamMap<>();
      map.put("array", object);
      Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
      return map;
    }
    return object;
  }

从源码分析上可以得知,我们在SQL中可以通过以下参数名引用到参数变量:

  1. @Param指定的参数名;
  2. 如果我们不使用@Param注解,则会默认使用参数的本名作为参数名;
  3. 无论是否使用@Param,param1、param2等根据参数索引和前缀param拼接生成的名称可以用于参数引用;

你可能感兴趣的:(Java,#,mybatis,mybatis,java,开发语言)