Why can Spring MVC get the method parameter name, but MyBatis can't? [Enjoy Spring MVC]

Keywords: Java Spring Mybatis jvm

Every sentence

Hu Shi: Talk more about problems and less about doctrine

Preface

Spring MVC and MyBatis are the two most popular frameworks at present, which are used in the development of daily life. If you take a step further to think, you should have such questions:

  • When using Spring MVC, you can automatically encapsulate values as long as the parameter name corresponds to the key of the request parameter, even if you don't use annotations.
  • When using MyBatis (interface mode), when the interface method passes parameters to the SQL statements in xml, the key value must be specified with @Param('') in order to be obtained in SQL (of course not 100% of the requirements, but not considered here in special cases).

I can't believe it's my own question, because I had this question the first time I used MyBatis and tried to get rid of the @Param annotation, because I thought it would be a bit redundant for me to write a name twice (I'm too lazy).
Compared with Spring MVC's humanized processing, MyBatis's handling of this piece was considered to be weak and explosive at that time. After so much confusion, today I can finally explain this phenomenon and unveil it.~

Problem discovery

Java users know that. java files belong to source files, which need to be compiled into. class bytecode files by javac compiler to be executed by JVM.
A buddy who knows a little bit about. class bytecode should also know this: Java does not reserve method parameter names by default when compiling methods, so if we want to get method parameter names directly from. class bytecode at runtime, we can't.

In the following cases, it is obvious that the real parameter names can not be obtained.

public static void main(String[] args) throws NoSuchMethodException {
    Method method = Main.class.getMethod("test1", String.class, Integer.class);
    int parameterCount = method.getParameterCount();
    Parameter[] parameters = method.getParameters();

    // Print Output:
    System.out.println("Total number of method parameters:" + parameterCount);
    Arrays.stream(parameters).forEach(p -> System.out.println(p.getType() + "----" + p.getName()));
}

Print content:

Total method parameters: 2
class java.lang.String----arg0
class java.lang.Integer----arg1

From the results, we can see that we can not get the real method parameter names (meaningless arg0, arg1, etc.). This result is in line with our theoretical knowledge and expectations.

If you are technically sensitive, then you should have the question: when using Spring MVC, Controller's method can be automatically encapsulated without annotations, like this:

@GetMapping("/test")
public Object test(String name, Integer age) {
    String value = name + "---" + age;
    System.out.println(value);
    return value;
}

Request: / test? Name = FSX & age = 18. Console output:

fsx---18

From the results, we can see: is it a bit incredible that Spring MVC can achieve the seemingly impossible case (get the method parameter name, and then complete the encapsulation)?

Look at this example again (the scenario where Spring MVC is restored to get parameter names):

public static void main(String[] args) throws NoSuchMethodException {
    Method method = Main.class.getMethod("test1", String.class, Integer.class);
    MethodParameter nameParameter = new MethodParameter(method, 0);
    MethodParameter ageParameter = new MethodParameter(method, 1);

    // Print Output:
    // Using Parameter output
    Parameter nameOriginParameter = nameParameter.getParameter();
    Parameter ageOriginParameter = ageParameter.getParameter();
    System.out.println("===================Source born Parameter Result=====================");
    System.out.println(nameOriginParameter.getType() + "----" + nameOriginParameter.getName());
    System.out.println(ageOriginParameter.getType() + "----" + ageOriginParameter.getName());
    System.out.println("===================MethodParameter Result=====================");
    System.out.println(nameParameter.getParameterType() + "----" + nameParameter.getParameterName());
    System.out.println(ageParameter.getParameterType() + "----" + ageParameter.getParameterName());
    System.out.println("==============Settings ParameterNameDiscoverer after MethodParameter Result===============");
    ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    nameParameter.initParameterNameDiscovery(parameterNameDiscoverer);
    ageParameter.initParameterNameDiscovery(parameterNameDiscoverer);
    System.out.println(nameParameter.getParameterType() + "----" + nameParameter.getParameterName());
    System.out.println(ageParameter.getParameterType() + "----" + ageParameter.getParameterName());
}

Output results:

===================Source born Parameter Result=====================
class java.lang.String----arg0
class java.lang.Integer----arg1
===================MethodParameter Result=====================
class java.lang.String----null
class java.lang.Integer----null
==============Settings ParameterNameDiscoverer after MethodParameter Result===============
class java.lang.String----name
class java.lang.Integer----age

It can be seen from the results that Spring MVC completes the acquisition of method parameter names with the help of Parameter Name Discoverer, and then completes data encapsulation. For an explanation of ParameterName Discoverer, you can first refer to: Spring Standard Processing Component Collection (Parameter Name Discoverer, Autowire Candidate Resolver, Resolvable Type...). )

This question introduces the basic usage and capabilities of ParameterName Discoverer, but does not provide in-depth analysis. So this article will analyze why Spring MVC can correctly parse method parameter names, and analyze the reasons from the perspective of bytecode.~

For ease of understanding, let's briefly talk about two concepts in bytecode: Local Variable Table and LineNumberTable. The two brothers are often taken out to talk together. Of course, the focus of this article is Local Variable Table, but they also take this opportunity to bring LineNumberTable with them.

LineNumberTable

Have you ever wondered why the line number displayed when an online program throws an exception happens to be the line of your source code? The reason for this question is that JVM executes A. class file, and the lines of the file and. java source files are certainly not corresponding. Why can the line numbers correspond in. Java files?
This is what LineNumberTable does: The LineNumberTable attribute exists in the code (bytecode) attribute, which establishes the link between bytecode offset and the line number of the source code.

LocalVariableTable

The LocalVariableTable attribute establishes the correspondence between the local variables in the method and the local variables in the source code. This property also exists in the code (bytecode)~
From the name, it can be seen that it is a set of local variables. Describes local variables and descriptors as well as the corresponding relationship with source code.

Next, I use javac and javap commands to demonstrate this situation:
The java source code is as follows:

package com.fsx.maintest;
public class MainTest2 {
    public String testArgName(String name,Integer age){
        return null;
    }
}

Note: I write the source code at the top, so please pay attention to the line number.~

Use javac MainTest2.java to compile. class bytecode, and then use javap -verbose MainTest2.class to view the bytecode information as follows:

As you can see from the figure, my red line number is exactly the same as the line number at the source code, which answers the question that the line number above corresponds to: LineNumberTable, which records the line number at the source code.
Tips: No, no, no Local Variable Table.

The source code remains unchanged. I use javac-g MainTest2. java to compile, and then look at the corresponding bytecode information as follows (note the difference from the above):

Here is an additional Local Variable Table, the local variable table, which records the names of the parameters that our method enters. Now that it's recorded, we can get the name by analyzing the bytecode information.~

Note: The debugging options of javac mainly include three sub-options: lines, source, vars
If - g is not used for compilation, only source files and line number information are retained; if - g is used for compilation, then all are available.~

What's the difference between parameters and-parameters?

You know less about - g compilation parameters, but you know more about Java 8's new - parameters. So what's the difference between it and the - g parameter???

I prefer to give myself an example to illustrate the problem. The source code of java is as follows:

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class MainTest2 {

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MainTest2.class.getMethod("testArgName", String.class, Integer.class);
        System.out.println("paramCount:" + method.getParameterCount());
        for (Parameter parameter : method.getParameters()) {
            System.out.println(parameter.getType().getName() + "-->" + parameter.getName());
        }
    }

    public String testArgName(String name, Integer age) {
        return null;
    }
}

The following are compiled and executed with javac, javac-g and javac-parameters respectively. The results are as follows:

From the results of compiling, running and printing separately, the results and their differences are very clear, so I will no longer pen and ink, if I have any questions, I can leave a message.

In addition, the bytecode information compiled by - parameters is attached for your analysis and comparison.

== Introduction of Three Ways to Get Method Parameter Names==

Although the Java compiler wipes out the method's parameter name by default, we still have a way to get the method's parameter name based on the knowledge of bytecode described above. The following three schemes are introduced for reference.

Method 1: Use - parameters

In the simplest and most direct way, Java 8 source support is available directly from java.lang.reflect.Parameter, as follows:

public class MainTest2 {

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = MainTest2.class.getMethod("testArgName", String.class, Integer.class);
        System.out.println("paramCount:" + method.getParameterCount());
        for (Parameter parameter : method.getParameters()) {
            System.out.println(parameter.getType().getName() + "-->" + parameter.getName());
        }
    }

    public String testArgName(String name, Integer age) {
        return null;
    }
}

Output:

paramCount:2
java.lang.String-->name
java.lang.Integer-->age

Of course, it has two biggest drawbacks:

  1. Must be Java 8 or above (because Java 8 is already very popular, so this is OK)
  2. Compilation parameters must have - parameters (relying on compilation parameters makes migration less friendly, which is fatal)

Specify how - parameters compile parameters:

  1. Manual command compilation: javac-parameters XXX.java
  2. IDE (for example, Idea) compiles:
  3. Maven compilation: Ensure the correctness of project migration by specifying compiler plug-ins (recommended)
<!-- Compilation environment 1.8 Compile and attach compile parameters:-parameters-->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <compilerVersion>${java.version}</compilerVersion>
        <encoding>${project.build.sourceEncoding}</encoding>
    </configuration>
</plugin>

Advantages: Simple and convenient
Disadvantage: It's inconvenient to specify specific - parameters (of course, using the maven editing plug-in to specify a relatively reliable solution and recommend it)

Solution 2: Use the - g + javap command

For example, the above example can be compiled using javac-g, then get bytecode information using javap, and then extract the parameter name according to the format of the information (do it yourself, do it yourself)

It's like letting you parse the http protocol by yourself. Would you like to do that??? So although this method is a method, it is obviously inconvenient to adopt.

Scheme 3: With the help of ASM (Recommendation)

Speaking of ASM, young partners should at least be familiar with the name. It is a Java bytecode manipulation framework. It can be used to dynamically generate classes or enhance the functions of existing classes. It can change class behavior, analyze class information, and even generate new classes according to user requirements.

For ASM, Java class is described as a tree; Visitor mode is used to traverse the entire binary structure; event-driven processing allows users to focus only on the meaningful parts of their programming (for example, this article only focuses on method parameters, others do not care), rather than Java. All the details of the class file format.

Interlude: On Proxy, CGLIB, Javassist, ASM:

  1. ASM: Java bytecode open source manipulation framework. The level of manipulation is the assembly instruction level of the underlying JVM, which requires users to have a certain understanding of the class organization structure and JVM assembly instructions.
  2. Javassist: The same effect. Compared with ASM, it is characterized by simple operation and speed (of course, not ASM fast). Important: It does not require you to understand JVM instructions / assembly instructions~
  3. Proxy dynamic proxy: dynamically generated (not compiled in advance) proxy class: $Proxy 0 extends Proxy implements MyInterface {...}, which determines that it can only proxy interfaces (or classes that implement interfaces), and single inheritance mechanism also determines that it cannot proxy (abstract) classes.~
  4. CGLIB: It is a powerful, high performance and high quality bytecode generation library based on ASM. It can extend Java classes and implement Java interfaces at runtime.

    > Spring AOP and Hibernate use CGLIB in creating proxy objects

In the previous article, I introduced the use of the CGLIB API directly to manipulate bytecodes/generate proxy objects. This article will briefly demonstrate an example of using the ASM framework directly:

Examples of ASM usage

First import the asm dependency package:

<!-- https://mvnrepository.com/artifact/asm/asm -->
<dependency>
    <groupId>asm</groupId>
    <artifactId>asm</artifactId>
    <version>3.3.1</version>
</dependency>

Description: asm has been upgraded to version 7.x and GAV has changed. Because I'm familiar with 3.x, I'm still old-fashioned here.~

Based on ASM, a tool method getMethodParamNames(Method) is provided to obtain the entry name of any method:

public class MainTest2 {

    // Enrollees who get the specified Method (returns an array, in order)
    public static String[] getMethodParamNames(Method method) throws IOException {
        String methodName = method.getName();
        Class<?>[] methodParameterTypes = method.getParameterTypes();
        int methodParameterCount = methodParameterTypes.length;
        String className = method.getDeclaringClass().getName();
        boolean isStatic = Modifier.isStatic(method.getModifiers());
        String[] methodParametersNames = new String[methodParameterCount];


        // Use org.objectweb.asm.ClassReader to read this method
        ClassReader cr = new ClassReader(className);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);


        // This step is the most important. visitor browse is started.
        // ClassAdapter is a subclass of org.objectweb.asm.ClassVisitor~~~~
        cr.accept(new ClassAdapter(cw) {

            // Because we only care about browsing the method here, we just need to copy the method here.
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                final Type[] argTypes = Type.getArgumentTypes(desc);

                // visitor-only methods with the same method name and parameter type~~~
                if (!methodName.equals(name) || !matchTypes(argTypes, methodParameterTypes)) {
                    return mv;
                }

                // Construct a Method Visitor to return to rewrite the method visitLocalVariable that we care about~~~
                return new MethodAdapter(mv) {

                    //Particular note: if it is a static method, the first parameter is the method parameter, and if it is a non-static method, the first parameter is this, and then the method parameter.
                    @Override
                    public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
                        // Handling static methods or not~~
                        int methodParameterIndex = isStatic ? index : index - 1;
                        if (0 <= methodParameterIndex && methodParameterIndex < methodParameterCount) {
                            methodParametersNames[methodParameterIndex] = name;
                        }
                        super.visitLocalVariable(name, desc, signature, start, end, index);
                    }
                };
            }
        }, 0);
        return methodParametersNames;
    }

    /**
     * Comparing the consistency of parameters
     */
    private static boolean matchTypes(Type[] types, Class<?>[] parameterTypes) {
        if (types.length != parameterTypes.length) {
            return false;
        }
        for (int i = 0; i < types.length; i++) {
            if (!Type.getType(parameterTypes[i]).equals(types[i])) {
                return false;
            }
        }
        return true;
    }
}

Operational cases:

public class MainTest2 {
    
    // Use Tool Method to Get Method's Input Name~~~
    public static void main(String[] args) throws SecurityException, NoSuchMethodException, IOException {
        Method method = MainTest2.class.getDeclaredMethod("testArgName", String.class, Integer.class);
        String[] methodParamNames = getMethodParamNames(method);

        // Print Output
        System.out.println(StringUtils.arrayToCommaDelimitedString(methodParamNames));
    }

    private String testArgName(String name, Integer age) {
        return null;
    }
}

Output:

name,age

Composite effect expectation, using ASM to get the actual method parameter name we expect (no compilation parameters are specified). Using ASM-based methods, even if you are under Java 8, you can get it normally, because it does not depend on compilation parameters.~~~

== With these basic knowledge, the book returns to its original form to explain the questions at the beginning of the article.==

Why does Spring MVC work well?

First of all, it needs to be clear: Spring MVC works well, but it does not depend on the - parameters parameter, nor does it depend on the - g compiler parameter, because it is implemented with ASM.~

In spring-core, a parameter Name Discoverer is used to get parameter names. The underlying is asm parsing, but the parameter names of interface methods can not be obtained, that is, the method parameter names of non-interface classes can only be obtained.
From the example at the beginning of this article, we can see that Spring MVC ultimately relies on DefaultParameter Name Discoverer to help get the entry name. Look at this code:

// @since 4.0
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {

    public DefaultParameterNameDiscoverer() {
        if (!GraalDetector.inImageCode()) {
            if (KotlinDetector.isKotlinReflectPresent()) {
                addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
            }
            addDiscoverer(new StandardReflectionParameterNameDiscoverer());
            addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
        }
    }
}

DefaultParameterName Discoverer is a reflection of the responsibility chain pattern, which is handled by the implementation classes it adds, that is, the two brothers:
Standard ReflectionParameter Name Discoverer: Depending on - parameters is effective (with java version requirements and compilation parameters requirements)
Local Variable TableParameter Name Discoverer: Based on ASM implementation, no version and compilation parameter requirements~

Note: Spring uses ASM without additional guides because it is self-sufficient:

Why does MyBatis not work well?

First, it needs to be made clear that MyBatis is implemented by binding interfaces to SQL statements and then generating proxy classes.

Now that there is a powerful ASM, the question arises: can ASM not help MyBatis simplify development?
Look at the example I gave you and you may see that MyBatis is not to blame.

public class MainTest2 {
    
    // Use Tool Method to Get Method's Input Name~~~
    public static void main(String[] args) throws SecurityException, NoSuchMethodException, IOException {
        Method method = MainTest2.class.getDeclaredMethod("testArgName", String.class, Integer.class);
        String[] methodParamNames = getMethodParamNames(method);

        // Print Output
        System.out.println(StringUtils.arrayToCommaDelimitedString(methodParamNames));
    }
}

// Interface method
interface MyDemoInterface{
    String testArgName(String name, Integer age);
}

Output:

null,null

It can be seen that even better than ASM, wood has a way to get the formal parameters of the interface directly.
This is understandable, because the interface method is not a practical method, and its formal parameters will be overwritten by the implementation class, so the formal parameters of the interface method are of little significance.~

Tips: The parameter names of default method and static method on the interface can be obtained normally, and interested partners can try them by themselves.~

As for why ASM is ineffective for interfaces, the root cause is clear when I show the bytecode:

Because the abstract method has no method body, no local variables, and naturally no local variable table, even using ASM can not get its variable name.~

Note: After Java 8, using the - parameter parameter parameter, even the interface, can get the name of the entry directly through the Method, which is good for MyBatis. Of course, in order to ensure compatibility, I suggest that you use the @Param annotation to specify it obediently.~

So far, I have reason to believe that my little buddy is like me, fully understand why Spring MVC can, but MyBatis can't.~~~

summary

This article goes deep into the bytecode to analyze the problem that may have troubled you for a long time (such as the question), hoping to answer your questions. It also introduces the basic usage of ASM, which may be helpful for your subsequent understanding of other frameworks.~

Relevant Reading

Spring Standard Processing Component Collection (Parameter Name Discoverer, Autowire Candidate Resolver, Resolvable Type...). )

Knowledge exchange

== The last: If you think this is helpful to you, you might as well give a compliment. Of course, sharing your circle of friends so that more small partners can see it is also authorized by the author himself.~==

If you are interested in technical content, you can join the wx group: Java Senior Engineer and Architect Group.
If the group two-dimensional code fails, Please add wx number: fsx641385712 (or scan the wx two-dimensional code below). And note: "java into the group" words, will be manually invited to join the group

If there is a crack/typesetting problem, please click: Text Link - Text Link - Text Link

Posted by jdog5 on Thu, 15 Aug 2019 07:47:54 -0700