前言
Spring MVC和MyBatis作为当下最为流行的两个框架,大家平时开发中都在用。如果你往深了一步去思考,你应该会有这样的疑问:
- 在使用Spring MVC的时候,你即使不使用注解,只要参数名和请求参数的key对应上了,就能自动完成数值的封装
- 在使用MyBatis(接口模式)时,接口方法向xml里的SQL语句传参时,必须(当然不是100%的必须,特殊情况此处不做考虑)使用@Param('')指定key值,在SQL中才可以取到
我敢相信这绝不是我一个人的疑问,因为我在第一次使用MyBatis的时候就产生过这个疑问并且也尝试过去掉@Param注解,因为我觉得一个名称让我写两次是有点多此一举的(我太懒了)。
和Spring MVC人性化处理比起来,当时觉得MyBatis对这块的处理简直弱爆了。费解了这么长时间,今天我终于可以解释这个现象了,来揭开它的面纱~
问题发现
java使用者都知道,.java文件属于源码文件,它需要经过了javac编译器编译为.class字节码文件才能被JVM执行的。
对.class字节码稍微有点了解的小伙伴应该也知道这一点:Java在编译的时候对于方法,默认是不会保留方法参数名,因此如果我们在运行期想从.class字节码里直接拿到方法的参数名是做不到的。
如下案例,很明显就是获取不到真实参数名喽:
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(); // 打印输出: System.out.println("方法参数总数:" + parameterCount); Arrays.stream(parameters).forEach(p -> System.out.println(p.getType() + "----" + p.getName())); }
打印内容:
方法参数总数:2 class java.lang.String----arg0 class java.lang.Integer----arg1
从结果中可以看到我们并不能获取到真实方法参数名(获取到的是无意义的arg0、arg1等),这个结果符合我们的理论知识以及预期。
若你有一定技术敏感性,这个时候你应该有这样的疑问:在使用Spring MVC的时候,Controller的方法中不使用注解一样可以自动封装啊,形如这样:
@GetMapping("/test") public Object test(String name, Integer age) { String value = name + "---" + age; System.out.println(value); return value; }
请求:/test?name=fsx&age=18
。控制台输出:
fsx---18
从结果中可见:看似办不到的case,Spring MVC
竟然给做到了(获取到了方法参数名,进而完成封装),是不是有点不可思议???
再看此例(还原Spring MVC获取参数名的场景):
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); // 打印输出: // 使用Parameter输出 Parameter nameOriginParameter = nameParameter.getParameter(); Parameter ageOriginParameter = ageParameter.getParameter(); System.out.println("===================源生Parameter结果====================="); System.out.println(nameOriginParameter.getType() + "----" + nameOriginParameter.getName()); System.out.println(ageOriginParameter.getType() + "----" + ageOriginParameter.getName()); System.out.println("===================MethodParameter结果====================="); System.out.println(nameParameter.getParameterType() + "----" + nameParameter.getParameterName()); System.out.println(ageParameter.getParameterType() + "----" + ageParameter.getParameterName()); System.out.println("==============设置上ParameterNameDiscoverer后MethodParameter结果==============="); ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); nameParameter.initParameterNameDiscovery(parameterNameDiscoverer); ageParameter.initParameterNameDiscovery(parameterNameDiscoverer); System.out.println(nameParameter.getParameterType() + "----" + nameParameter.getParameterName()); System.out.println(ageParameter.getParameterType() + "----" + ageParameter.getParameterName()); }
输出结果:
===================源生Parameter结果===================== class java.lang.String----arg0 class java.lang.Integer----arg1 ===================MethodParameter结果===================== class java.lang.String----null class java.lang.Integer----null ==============设置上ParameterNameDiscoverer后MethodParameter结果=============== class java.lang.String----name class java.lang.Integer----age
从结果能看出来:Spring MVC借助ParameterNameDiscoverer完成了方法参数名的获取,进而完成数据封装。关于ParameterNameDiscoverer它的讲解,可先行参阅:【小家Spring】Spring标准处理组件大合集(ParameterNameDiscoverer、AutowireCandidateResolver、ResolvableType。。。)
该问介绍了ParameterNameDiscoverer的基本使用和提供的能力,但并没有深入分析。那么本文就分析为何Spring MVC为何可以正确的解析到方法参数名称这个问题,从字节码角度深入分析其缘由~
为了便于理解,先简单说说字节码中的两个概念:LocalVariableTable和LineNumberTable。它哥俩经常被拿出来一起说,当然本文关注的焦点是LocalVariableTable,但也借此机会一笔带过LineNumberTable。
LineNumberTable
你是否曾经疑问过:线上程序抛出异常时显示的行号,为啥就恰好就是你源码的那一行呢?有这疑问是因为JVM执行的是.class文件,而该文件的行和.java源文件的行肯定是对应不上的,为何行号却能在.java文件里对应上?
这就是LineNumberTable它的作用了:LineNumberTable属性存在于代码(字节码)属性中, 它建立了字节码偏移量到源代码行号之间的联系
LocalVariableTable
LocalVariableTable属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。这个属性也是存在于代码(字节码)中~
从名字可以看出来:它是局部变量的一个集合。描述了局部变量和描述符以及和源代码的对应关系。
下面我使用javac和javap命令来演示一下这个情况:
.java源码如下:
package com.fsx.maintest; public class MainTest2 { public String testArgName(String name,Integer age){ return null; } }
说明:源码我都是顶头写的,所以请注意行号~
使用javac MainTest2.java编译成.class字节码,然后使用javap -verbose MainTest2.class查看该字节码信息如下:
从图中可看到,我红色标注出的行号和源码处完全一样,这就解答了我们上面的行号对应的疑问了:LineNumberTable它记录着在源代码处的行号。
Tips:此处并没有,并没有,并没有LocalVariableTable。
源码不变,我使用javac -g MainTest2.java来编译,再看看对应的字节码信息如下(注意和上面的区别):
这里多了一个LocalVariableTable,即局部变量表,就记录着我们方法入参的形参名字。既然记录着了,这样我们就可以通过分析字节码信息来得到这个名称了~
说明:javac的调试选项主要包含了三个子选项:lines,source,vars
如果不使用-g来编译,只保留源文件和行号信息;如果使用-g来编译那就都有了~
和-parameters有什么区别??
知道-g编译参数的少,反倒对Java8新推出的-parameters知道的人更多一些。那么它和-g参数有什么区别呢???
百闻不如一见,我比较喜欢自己搞个例子来说明问题,.java源代码如下:
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; } }
下面分别用javac、javac -g、javac -parameters来编译后再执行,结果图如下:
从分别编译、再运行打印的结果图中看,结果以及他们的区别已经很清晰了,我就不再笔墨,有疑问的可以给我留言。
另外附上-parameters
编译后的字节码信息,方便你做分析对比: