spring太强了,深度解析:Spring MVC 如何巧妙获取方法参数名
2024-09-11 14:01 阅读(267)

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

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

例如我前些日子分享的日志工具,UserLog 注解可实现从 UserOrder中提取 userId 和 orderId,并将其自动注入到日志中。

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

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

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

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

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

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

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 的参数名称。

关键代码解读



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



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



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



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

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

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


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 编译时,可添加编译参数 -parameters。


<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版本的实现源码。

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 获取参数名的。

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

7. 总结


使用Spring 工具类 获取参数名 DefaultParameterNameDiscoverer

获取参数名有两种原理 1) jdk 反射方法,但需要添加编译参数 -parameters; 2)解析字节码

Spring Aop 可通过 JoinPoint 获取参数名


作者:五阳

链接:https://juejin.cn