spring真的太强了,深度解析,Spring MVC 如何巧妙获取方法参数名

1. 如何通过 Java 反射获取方法的参数名?

获取参数名是一个非常有用的技巧,例如 Spring MVC Controller 中可以根据参数名自动注入对应参数值。不仅Spring框架如此,我们自己开发的框架有时候也需要此项能力。

例如我前些日子分享的日志工具,UserLog 注解可实现从 UserOrder中提取 userId 和 orderId,并将其自动注入到日志中。分享一个简单实用的日志打印工具,使用两年了,极其方便

 

js

代码解读

复制代码

@UserLog(userId = "userId", orderId = "orderId") public void orderPerform(UserOrder order) { log.warn("订单履约完成"); }

但是框架有个缺陷,它假定了 userId 必须在 UserOrder 中,如果方法中直接声明 userId 的方式,则无法使用。例如以下方式,直接声明userId,框架就无法使用了。因为框架拿不到参数名,所以只能取第一个参数,从第一个参数中通过反射取属性值。

 

js

代码解读

复制代码

public void orderPerform(long userId, long orderId) { log.warn("订单履约完成"); }

如果可以获取到方法的参数名,就能和日志占位符匹配起来,然而如何获取方法的参数名呢?为什么获取到的参数名是 arg0,arg1 呢?

2. Spring 提供工具类获取参数名称

DefaultParameterNameDiscoverer Spring 工具类可以获取到参数名称列表,例如以下代码展示了该工具类的使用方式。

 

js

代码解读

复制代码

public class TestParamName { public void testName(String name, int value, Integer value2) { } @Test public void test1() throws Exception { //获取参数名称 工具类 DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); Method method = ReflectionUtils.findMethod(TestParamName.class, "testName", null); //获取参数名称列表 String[] paramNames = parameterNameDiscoverer.getParameterNames(method); for (String paramName : paramNames) { System.out.println(paramName); } } }

执行以上测试,会输出 testName 的参数名称。

关键代码解读

  1. DefaultParameterNameDiscoverer.getParameterNames(method) 方法 可以获取到 testName 的参数名列表。

  2. new DefaultParameterNameDiscoverer() ,工具类提供了无参的构造方法,无需任何参数,也无需关联 Spring ApplicationContext。

  3. Spring 提供了工具类 ReflectionUtils 来获取 Method 对象,使用 getMethod 时需注意:除了方法名,还要提供参数类型以精确匹配,因为 Java 允许方法重载。如果希望忽略参数列表,可以输入 null,否则 getMethod 会认为要获取的是无参方法。

获取参数名称有两种原理,反射方式和字节码解析方式。

3. 原理解析———jdk 反射方式获取参数名

Java 1.8 引入了通过反射获取方法参数名的功能。JDK 中的 Parameter 类提供了 getName 方法,用于获取参数名称。然而,在首次测试时返回的参数名称却是 arg0、arg1、arg2。造成这种情况的原因是,默认情况下反射无法获取方法的真实参数名。要解决这一问题,需要在编译时添加 -parameters 参数。

 

js

代码解读

复制代码

for (Parameter parameter : method.getParameters()) { //获取参数名称,未开启 -parameters 则返回 arg0。 System.out.println("Param: " + parameter.getName()); }

3.1 Idea设置编译参数

按照路径 settings/Build/Compiler/Java Compiler设置 编译参数,需要注意每次修改完编译参数后,需要 使用 Idea Maven 组件 重新编译,clean package 才能生效。

切记Idea Maven工具和 命令行 mvn 命令编译后的结果位置可能不同。修改 Idea 编译配置,则通过 idea maven工具重新编译。 

3.1.1 设置编译参数后的结果

设置编译参数,并且重新编译后,可以正确输出参数名称。

3.2 maven 方式设置编译参数

maven 编译时,可添加

整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

编译参数 -parameters。

 

xml

代码解读

复制代码

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>

在修改 Maven 配置后,需要在命令行中执行 mvn 命令重新编译项目,然后再使用 mvn 命令运行单元测试,以确保能够得到预期的结果。

需要注意的是,Idea 中的 Maven 工具和命令行中的 mvn 命令编译后的结果位置可能不同。因此,仅修改 Maven 配置而不更改 Idea 编译参数配置的情况下,在使用 Idea 调试单元测试时,仍然可能会无法获取到参数名称。

重新编译项目,执行单测 mvn clean package test -Dtest=TestParamName

除 jdk 反射方式外,还有解析字节码的方式。

4. 原理解析 ——— 解析本地变量表

使用 javap -v TestParamName.class 查看字节码文件时,可以看到字节码中包含方法名、参数名列表信息。

想要解析字节码,需要读取字节码文件,然后按照字节码规范解析。不过 Spring 借助 asm 字节码分析工具,完成了解析工作。 在 LocalVariableTableParameterNameDiscoverer 实现了字节码的解析和参数名获取。

如下代码截图显示, Spring 首先根据 Class对象获取字节码文件,然后解析内容。 

需要注意的是:解析字节码的方式不受限于 jdk 版本,在低版本也可以使用。

介绍完成获取参数名的两种原理后,很容易理解 Spring DefaultParameterNameDiscoverer 的实现原理。

5. Spring 的源码解析

DefaultParameterNameDiscoverer 在 Spring 的不同版本实现不同,但是背后原理大同小异 。参考 Spring 4.3.5版本的实现源码。

 

js

代码解读

复制代码

public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer { private static final boolean standardReflectionAvailable = ClassUtils.isPresent( "java.lang.reflect.Executable", DefaultParameterNameDiscoverer.class.getClassLoader()); public DefaultParameterNameDiscoverer() { if (standardReflectionAvailable) { addDiscoverer(new StandardReflectionParameterNameDiscoverer()); } addDiscoverer(new LocalVariableTableParameterNameDiscoverer()); } @Override public String[] getParameterNames(Method method) { for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { String[] result = pnd.getParameterNames(method); if (result != null) { return result; } } return null; } }

  • 反射方式获取参数名: StandardReflectionParameterNameDiscoverer
  • 解析字节码方式 : LocalVariableTableParameterNameDiscoverer

因为 受限于 jdk 版本和编译参数问题,无法稳定地使用 Java 反射方式获取参数名,所以 Spring 集成了两种方式,确保准确地拿到参数名。

以上代码中,Spring 优先判断当前版本是否在 1.8 及以后,如果是则使用 反射方式获取参数名列表; 否则将使用 ASM 解析字节码,从本地变量表获取参数名。 需要说明的是 java.lang.reflect.Executable 是jdk 1.8 以后提供的反射工具类。通过判断该类是否存在,来判断当前是否使用反射方式获取参数名。

getParameterNames 方法使用责任链模式,优先使用反射方式,如果反射方法无法获取到参数名,则使用本地变量表解析字节码。

有了 Spring 封装工具,我们无需再重复造轮子。

6. Spring AOP 获取参数名

Spring Aop 可以通过 getSignature()).getParameterNames 获取参数名。通过查看 getParameterNames 源码会发现,Spring 也是通过 DefaultParameterNameDiscoverer 获取参数名的。

 

js

代码解读

复制代码

public Object around(ProceedingJoinPoint joinPoint) throws Throwable { String[] params = ((MethodSignature) joinPoint.getSignature()).getParameterNames(); }

7. 总结

  1. 使用Spring 工具类 获取参数名 DefaultParameterNameDiscoverer
  2. 获取参数名有两种原理 1) jdk 反射方法,但需要添加编译参数 -parameters; 2)解析字节码
  3. Spring Aop 可通过 JoinPoint 获取参数名
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值