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 获取参数名
我的开源项目
最后夹带一点私货,五阳最近花了3个月的时间完成一个开源项目。
开源3周以来,已有近 230 多个关注和Fork
Gitee:gitee.com/juejinwuyan…
GitHub github.com/juejin-wuya…
开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是
- 功能堆砌,大部分功能用不上,需要大量裁剪;
- 逻辑差异点较多,需要大量修改;
- 功能之间耦合,难以独立替换某个功能。
由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显
所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?
- 他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
- 他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
- 他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响
开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?
memberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!
具体介绍可参见
Gitee开源地址:gitee.com/juejinwuyan…
GitHub开源地址 : github.com/juejin-wuya…
在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。
- Mybatis、Mybatis-plus 集成多数据源
- Sharding-jdbc 多数据源分库分表
- redis/redisson 缓存
- Apollo 分布式配置中心
- Spring Cloud 微服务全家桶
- RabbitMq 消息队列
- H2 内存数据库
- Swagger + Lombok + MapStruct
同时你也可以学习到以下组件的实现原理
- 流程引擎的实现原理
- 扩展点引擎实现原理
- 分布式重试组件实现原理
- 通用日志组件实现原理 参考:juejin.cn/post/740727…
- 商品库存实现原理: 参考:juejin.cn/post/731377…
- 分布式锁组件: 参考:
- Redis Lua的使用
- Spring 上下文工具类 参考: juejin.cn/post/746927…