前言
前段时间遇到一个需求,对接多方接口数据(同类数据但字段命名各异)转换为字段一致的标准化数据。
这个需求给我带来思考,因为接口很多且不确定,还在陆续增多,而且接口是有变化的,甚至不排除接口变化的风险,如果写死代价太大,因此这一步是必然要做成可配置的,例如把对方接口的a字段映射为a1字段,配置大概如下
json
[{from:"a",to:"a1"}]
但问题来了,有了这个映射的配置,如何实现呢?
最简单的方式就是使用反射,应对这种场景再适合不过了,但是接受数据量是很大的,使用反射就不得不考虑额外的性能开销。
查了很多资料最终参考了fastjson2再解决这种问题采用的思路,就是使用LambdaMetafactory。
因此引出本文,特意记录并测试下几种方法调用方案的性能差异
实测
采用 普通调用、反射、MethodHandle、LambdaMetafactory、 lambda几种方式,分别执行set方法10000000次,对比下性能差异
普通调用
java
public static void justSet() {
final StopWatch stopWatch = new StopWatch();
final Bean bean = new Bean();
stopWatch.start();
for (int i = 0; i < 10000000; i++) {
bean.setCode("abc");
}
System.out.println("Solution 1 just set: " + stopWatch.getTime());
stopWatch.stop();
}
反射
java
public static void reflection() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final StopWatch stopWatch = new StopWatch();
final Bean bean = new Bean();
final Method setCode = Bean.class.getMethod("setCode", String.class);
stopWatch.start();
for (int i = 0; i < 10000000; i++) {
setCode.invoke(bean, "abc");
}
System.out.println("Solution 2 reflection: " + stopWatch.getTime());
stopWatch.stop();
}
MethodHandle
java
public static void methodHandle() throws Throwable {
final StopWatch stopWatch = new StopWatch();
final Bean bean = new Bean();
// final MethodHandle setCode = MethodHandles.lookup().findSetter(Bean.class, "code", String.class); 由于code是私有方法,所以不能使用lookup
final MethodHandle setCode = MethodHandles.lookup().findVirtual(Bean.class, "setCode", methodType(void.class, String.class)); // lookup
stopWatch.start();
for (int i = 0; i < 10000000; i++) {
setCode.invokeExact(bean, "abc");
}
System.out.println("Solution 3 Method Handle: " + stopWatch.getTime());
stopWatch.stop();
}
LambdaMetafactory
java
public static void LambdaFactory() throws Throwable {
final StopWatch stopWatch = new StopWatch();
final Bean bean = new Bean();
final MethodHandles.Lookup lookup = MethodHandles.lookup();
// 函数式接口的方法签名:BiConsumer.accept(Object, Object)
MethodType samMethodType = MethodType.methodType(void.class, Object.class, Object.class);
// 实际调用签名:(Bean, String) -> void
MethodType instantiatedMethodType = MethodType.methodType(void.class, Bean.class, String.class);
final CallSite callSite = LambdaMetafactory.metafactory(lookup,
"accept",
MethodType.methodType(BiConsumer.class),
samMethodType,
lookup.findVirtual(Bean.class, "setCode", MethodType.methodType(void.class, String.class)),
instantiatedMethodType);
BiConsumer<Bean, String> setCode = (BiConsumer) callSite.getTarget().invokeExact();
stopWatch.start();
for (int i = 0; i < 10000000; i++) {
setCode.accept(bean, "abc");
}
System.out.println("Solution 4 lambda factory: " + stopWatch.getTime());
stopWatch.stop();
}
Lambda
java
public static void lambda() {
final StopWatch stopWatch = new StopWatch();
final Bean bean = new Bean();
final BiConsumer<Bean, String> setCode = Bean::setCode;
stopWatch.start();
for (int i = 0; i < 10000000; i++) {
setCode.accept(bean, "abc");
}
System.out.println("Solution 5 lambda: " + stopWatch.getTime());
stopWatch.stop();
}
测试结果如下(多次测试结果差不多):
yaml
Solution 1 just set: 6
Solution 2 reflection: 97
Solution 3 Method Handle: 43
Solution 4 lambda factory: 7
Solution 5 lambda: 6
可以看到,反射的性能是最差的,MethodHandle的性能要比反射好很多,但最快的还是最后的两种方式:LambdaMetafactory和lambda,和直接调用性能基本一致。
分析
反射
反射性能差是大名鼎鼎的,但这里不考虑获取Method的耗时,只是考虑invoke(实际调用方法)的性能。
反射是java1.1中纯java代码实现的,其中方法(包括invoke方法)本身逻辑复杂、分支判断多、目标不清晰。JIT看到的是一个超大的、复杂的、近乎黑盒的方法,无法JIT优化。
MethodHandle
MethodHandle是jdk1.7引入的,其诞生背景如下:
- Groovy/Scala 等动态语言跑在 JVM,需要高频动态调用,反射太慢;
- 准备做 Java8 Lambda,需要高性能动态调用;
MethodHandle不是普通Java类,是JVM原生结构,JIT专门针对它做了硬编码优化逻辑,所以它对JIT天生就是非常友好的,性能大大优于反射,可以JIT优化甚至内联。
LambdaMetafactory
虽然MethodHandle性能已经提高很多,但归根结底还是动态间接调用,JIT优化有上限,离原生直接调用还有一点差距。
因此JDK1.8引入了LambdaMetafactory,它利用MethodHandle,相当于生成一个实现函数式接口的类,里面就是一行普通直接方法调用。
java
class Proxy implements BiConsumer {
void accept(Bean b, String s) {
b.setCode(s); // 原生直接调用
}
}
这种几乎完全就和setCode的速度一致了,JIT优化、内联该有都有。
而fastjson2中,就是利用了LambdaMetafactory这项黑科技,才实现了性能上的提升。
Lambda
而以上这种lamdba写法:BiConsumer<Bean, String> setCode = Bean::setCode;,本质上其实就是和上面使用LambdaMetafactory生成的代码等价的,只是个语法糖。
显然这种写法更简单,但并不灵活,比如我遇到的场景和fastjson2的场景都是要根据字段的名字(字符串),建了和方法的对应关系,这种场景下只能复杂的LambdaMetafactory反而更灵活。
总结
各方法性能差异本质源于实现层级与JIT优化程度不同,但各有适用场景:
反射通用性最强,不仅可用于低频动态调用,元数据获取、注解解析也只能依赖反射;MethodHandle是JVM原生轻量调用原语,兼顾性能与动态性,是 Lambda 底层基础;LambdaMetafactory基MethodHandle 构建,动态生成直接调用代码,性能最优,适合高频调用场景。实际开发中按需选型即可。