ParamNameResolver is used by MyBatis to parse method parameters. We call the method of Mapper interface to pass the parameters. After being parsed by ParamNameResolver, the SQL statement in THE XML file can use these parameters, and the parameter filling of dynamic SQL and placeholders can be completed.

How does MyBatis parse parameters? How do you make better use of these parameters in XML? Today we will analyze it from the perspective of source code.

1. Source code analysis

The ParamNameResolver class structure is very simple, there is no inheritance and implementation, the source code is very easy to understand. It also has very simple responsibilities: parses method parameters to SortedMap, and maps parameter names to parameter values based on the arguments to get ParamMap.

1.1 attributes

public class ParamNameResolver {

  // Automatically generated parameter name prefix
  public static final String GENERIC_NAME_PREFIX = "param";

  // Whether to use the actual parameter name (reflection)
  private final boolean useActualParamName;

  // Parameter subscript - mapping of parameter names
  private final SortedMap<Integer, String> names;

  // Is there a @param annotation
  private boolean hasParamAnnotation;
}
Copy the code

MyBatis will use reflection to retrieve the parameter name if it is not annotated @param. However, when the source code is compiled into a bytecode file, the parameter names are virtually meaningless to the program’s execution, so the compiler defaults to removing the parameter names and reflects arg0,arg1 and other meaningless names. So how do I get the parameter name through reflection?

Only if the JDK version is 8 or later and the **-parameters** parameter is compiled can the actual parameter name be retrieved by reflection.

MyBatis provides a deterministic solution that automatically generates names for parameters. GENERIC_NAME_PREFIX is the automatically generated name prefix. The generation rule is: [param+ ordinal], so you can also use parameters in XML with param+ ordinal.

UseActualParamName indicates whether the actual parameter name, which is the parameter name obtained by reflection, is used. As mentioned above, the threshold for reflection to get real parameter names is relatively high, and the result is likely to be meaningless parameter names. This value defaults to true. When set to false, MyBatis automatically uses the subscript of the parameter as the parameter name. In this case, you can use the first parameter in XML by #{0}.

Names is an ordered Map container that stores the names of parameter subscripts in method parameters.

HasParamAnnotation records whether there’s an @param annotation in a method parameter.

1.2 Constructors

Properties, let’s look at constructors.

/** * 1@ParamAnnotated values * 2. Get the parameter name by reflection * 3. Use parameter subscript */
public ParamNameResolver(Configuration config, Method method) {
  Setting name="useActualParamName" value="true" />
  this.useActualParamName = config.isUseActualParamName();
  // Reflection method parameter type
  finalClass<? >[] paramTypes = method.getParameterTypes();// reflection gets parameter annotations
  final Annotation[][] paramAnnotations = method.getParameterAnnotations();
  // Map container containing parameter subscripts
  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 RowBounds and ResultHandler arguments, which are not parsed. * RowBounds: handles paging * ResultHandler: handles results */
      continue;
    }
    String name = null;
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      if (annotation instanceof Param) {
        // If @param annotation is added, the annotation value is taken
        hasParamAnnotation = true;
        name = ((Param) annotation).value();
        break; }}// No @param annotation
    if (name == null) {
      // @Param was not specified
      if (useActualParamName) {
        // If the actual parameter name is used, the parameter name is obtained by reflection.
        // JDK8 compiles the class with the -parameters parameter to keep the parameter name, otherwise, arg0,arg1, etc
        name = getActualParamName(method, paramIndex);
      }
      if (name == null) {
        // If the name is still empty, you can use subscripts to get parameters :#{param1},#{param2}...
        name = String.valueOf(map.size());
      }
    }
    map.put(paramIndex, name);
  }
  // make it immutable
  names = Collections.unmodifiableSortedMap(map);
}
Copy the code

Constructors do a couple of things:

  1. Reflection gets the parameter type.
  2. If the parameter type is RowBounds or ResultHandler, parsing is skipped.
  3. Reflection get method parameter annotation.
  4. If the @param annotation is added, the parameter name uses the annotation value directly.
  5. If there are no annotations, determine whether reflection is required to get the parameter name.
  6. Otherwise, use the parameter subscript as the parameter name.
  7. Map the relationship between parameter subscripts and parameter names to build a Map container.

The whole process is fairly straightforward, so here are a few examples.

void select(String name, int age);
Copy the code

The result of resolving names is:

-parameters {0:"arg0",
  1:"arg1"} -- do not use the actual parameter name {0:"0",
  1:"1"} -parameters is added at compile time, using the actual parameter name {0:"name",
  1:"age"
}
Copy the code

Annotated case:

void select(@Param("name") String name,@Param("age") int age);
Copy the code

The result of resolving names is:

{0:"name",
  1:"age"
}
Copy the code

If there are RowBounds or ResultHandler in the argument:

void select(@Param("name") String name,RowBounds rowBounds,ResultHandler resultHandler,@Param("age") int age);
Copy the code

The result of resolving names is:

Skip RowBounds and ResultHandler {0:"name",
  3:"age"
}
Copy the code

1.3 getNamedParams ()

After the constructor parses names, it knows the parameter names corresponding to the subscripts of the method parameters, and then calls the getNamedParams() method to parse the ParamMap mapping between the parameter names and the parameter values based on the arguments.

ParamMap inherits from HashMap, except that it limits the Key’s genericity to String. The name of the parameter must be a string, and the value of the parameter is uncertain. Use Object.

Here is the source code:

/** * get the Map of the parameter name. If there is only one parameter, return the Map@param args
 * @return* /
public Object getNamedParams(Object[] args) {
  // Number of arguments
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    // There is no parameter
    return null;
  } else if(! hasParamAnnotation && paramCount ==1) {
    // There is no @param annotation and only one argument
    Object value = args[names.firstKey()];
    // If the parameter number is a collection type, the parameter name is encapsulated as collection/list/array
    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()) {
      // The parameter name corresponds to the argument value
      param.put(entry.getValue(), args[entry.getKey()]);
      // add generic param names (param1, param2, ...)
      // Automatically generate an additional parameter name mapping :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++; }returnparam; }}Copy the code

This method results in a ParamMap containing the mapping between parameter names and parameter values, and is called by passing the method’s real parameter group.

It does several things:

  1. If the argument is empty, null is returned.
  2. There is no @param annotation and only one parameter.
    1. Is it a collection type (containing arrays)?
    2. If yes, the parameter name is automatically set to Collection /list/array.
    3. If no, the parameter value is returned.
  3. Create a ParamMap and add the parameter names and corresponding parameter values to the container.
  4. In addition, regenerate a mapping entry with the parameter name [param+ ordinal number].
  5. Return the Map container.

MyBatis will automatically generate the mapping with Collection/List /array as the parameter name if there is only one argument and there is no @param annotation and the parameter type is Collection or List or array. You can use it directly in XML, such as #{List}. The source code is as follows:

/* If it's a collection, it's a List. If it's an array, it's an array. If it's an array, it's an array
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;
}
Copy the code

2. The emphasis

2.1 How do I Obtain Parameter Names by Reflection?

The default access to arg0, arg1 such meaningless names, only the JDK version of 8 or above, and added to the parameters at compile time parameters to get the reflection.

2.2 SpringMVC can bind parameters according to parameter names. Why not MyBatis?

SpringMVC is not JDK8 version, there is no -parameters parameter, there is no annotation, still can dynamically bind parameters according to the parameter name, why MyBatis not? Must be annotated @param?

That’s because Spring’s Maven project automatically adds the -g parameter to the compilation package. This parameter tells the compiler that we need to debug the class information, and the compiler keeps the local variable table information at compile time. The parameter is also part of the local variable table. At this point, Spring parses the bytecode file through ASM to obtain the parameter name by changing the direction of the local variable table, and then performs dynamic binding of parameters according to the parameter name.

In summary, SpringMVC can obtain the parameter name because the -g parameter is added during compilation, and the information of [local variable table] is retained during compilation, and the parameter name is obtained by changing the direction of the local variable table. MyBatis Mapper is an interface, there is no method body, there is no local variable table, so ASM can not as an example.

3. Summary

MyBatis: ParamNameResolver: ParamNameResolver: ParamNameResolver It first parses the parameters of the method in the constructor to obtain the parameter name corresponding to the parameter subscript. Then it does the mapping according to the method arguments and finally obtains the mapping relationship between parameter name and parameter value ParamMap.

There are additional extensions to get parameter names, as compared to SpringMVC, which can get parameter names without annotations because the Controller method has a method body, and Mapper is an interface, so methods don’t have a method body, so they don’t have a local variable table, so they can’t get it.

In addition to parsing the parameter name, MyBatis will also generate an additional mapping relationship with [param+ serial number] as the parameter name, even if you miss the annotation, reflection can not get the parameter name, also at least one method can ensure the correct use of parameters.