Mybatis-plus parameter name analysis of spy series

ParamNameResolver

When we use MyBatis for database operations, we often need to write SQL statements, and we need to pass the parameters of the method to the parameter placeholders in the SQL statement. MyBatis provides a convenient way to achieve this, that is, use the parameter placeholder of the form #{paramName} and store the parameters of the method in a Map object, and then pass the Map object to the SQL statement executor. However, the method parameter name is not recorded in the Java bytecode, so MyBatis needs a way to get the name of the method parameter so that it can correctly pass the parameter value to the SQL statement.

To solve this problem, MyBatis provides a class called ParamNameResolver for resolving parameter names in methods. ParamNameResolver uses the getParameters() method provided by the Executable interface in Java’s reflection mechanism to obtain all parameter objects of the method, and then combines them with the bytecode The local variable table information, parse out the parameter name of the method. If the @Param annotation is used in the method, the parameter name in the annotation will be used as the name of the method parameter first.

The following is the core code snippet of 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
        //...
    }

    //...
}

The main function of ParamNameResolver is to provide MyBatis with a unified way to obtain the name of the method parameter, so that the parameter value can be passed to the SQL statement correctly. In MyBatis, if the parameter placeholder in the SQL statement uses the form of #{paramName}, then MyBatis will obtain the corresponding parameter name through ParamNameResolver, And obtain the corresponding parameter value from the parameter Map object passed by the executor, and bind it to the parameter placeholder in the SQL statement.

In short, ParamNameResolver is an important component in MyBatis, which provides a convenient way for MyBatis to obtain the names of method parameters, so that MyBatis can implement SQL statements and Java methods more flexibly and conveniently passing parameters between.

This code is the constructor implementation of the ParamNameResolver class in MyBatis. The main function is to parse the names of method parameters and map the parameter names and their indices into a SortedMap.

First, the constructor will determine whether to use the actual parameter name according to the configuration of MyBatis. Then, get all parameter types and annotations of the method, as well as the number of parameters. Then, for each parameter, the name of the parameter is obtained by traversing its annotations. If no @Param annotation is found, the default name is used.

If Use Actual Parameter Name is enabled, and the parameter does not have a name specified, an attempt is made to obtain the actual name of the parameter from the method. If the name is still not found, the index of the argument is used as the name.

Finally, the constructor saves the parameter name and index mapping relationship in a SortedMap and encapsulates it into an unmodifiable SortedMap.

It should be noted that in the code, the isSpecialParameter method is used to determine whether the parameter is a special parameter. These special parameters include types such as RowBounds, ResultHandler, and ParamMap. These parameters usually do not need to specify names.

In the for loop, use break to end the loop early, which can avoid unnecessary traversal and improve the efficiency of the code. Also, since names is an unmodifiable SortedMap, you can use the Collections.unmodifiableSortedMap method to create an unmodifiable map.

In general, the role of this constructor is to parse the names of the method parameters and map the parameter names and their indices into a SortedMap. This SortedMap can be conveniently used for subsequent parameter parsing operations, so that MyBatis can handle method parameters more flexibly.

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

In MyBatis, the getNamedParams method is used to obtain the parameter names and their corresponding parameter values that have been annotated with @Param in the method parameter list, and those that have not used @Param Annotated parameter names and their corresponding parameter values.

Returns null if the number of arguments is 0 or the args array is null. If the number of parameters is 1 and the @Param annotation is not used, the value of the parameter is directly used, converted into a Map object and returned. Otherwise, save the mapping of parameters and names into a Map and return that Map.

The code first creates a ParamMap object, then traverses names and args, takes out the mapping relationship between parameters and names, and adds it to the Map. At the same time, a counter variable i is also used to generate parameter names using the default naming rules. These parameter names start with “param” and end with numbers, such as “param1”, “param2”, etc., and then add them to the Map .

When adding parameters, the code will first add the parameter names in names, and then add parameters using the default naming rules, and avoid overwriting parameters that already have names.

 public Object getNamedParams(Object[] args) {<!-- -->
    final int paramCount = names. size();
    if (args == null || paramCount == 0) {<!-- -->
      return null;
    } else if (!hasParamAnnotation & amp; & amp; 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 is used to wrap the parameters of collection or array type in the parameter list of the method into a Map object.

If the parameter object is a collection type, a ParamMap object will be created and the collection object will be stored in the Map object. At the same time, a key named collection will be added, and the corresponding value will be a collection object. At the same time, if actualParamName is not empty, add a key named actualParamName to the ParamMap object, and the corresponding value is also an array object.

If the collection object is still of type List, a key named list will be added, and the corresponding value will also be a collection object.

If the parameter object is an array type, similar to the collection type, a ParamMap object will also be created, and a key named array will be added, and the corresponding value will be an array object. At the same time, if actualParamName is not empty, add a key named actualParamName to the ParamMap object, and the corresponding value is also an array object.

If the parameter object is not a collection or array type, object will be returned directly without any processing. Because there is only one parameter, it does not make much practical sense to wrap it into a Map object, and it is simpler and more efficient to return the object directly.

It should be noted that ParamMap is a special Map implementation in MyBatis, which can be used to store multiple key-value pairs with the same name. In this way, we can use the #{paramName} placeholder in the SQL statement to refer to the parameter without worrying about the repetition of the parameter name. At the same time, ParamMap can also be used like a normal Map, supporting operations such as adding, deleting, and traversing.

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

From the analysis of the source code, we can know that we can refer to parameter variables through the following parameter names in SQL:

  1. The parameter name specified by @Param;
  2. If we don’t use the @Param annotation, the real name of the parameter will be used as the parameter name by default;
  3. No matter whether @Param is used or not, the names generated by concatenation of param1, param2, etc. based on parameter index and prefix param can be used for parameter reference;