LambdaMetafactory(fastjson2使用的黑科技)

前言

前段时间遇到一个需求,对接多方接口数据(同类数据但字段命名各异)转换为字段一致的标准化数据。

这个需求给我带来思考,因为接口很多且不确定,还在陆续增多,而且接口是有变化的,甚至不排除接口变化的风险,如果写死代价太大,因此这一步是必然要做成可配置的,例如把对方接口的a字段映射为a1字段,配置大概如下

json 复制代码
[{from:"a",to:"a1"}]

但问题来了,有了这个映射的配置,如何实现呢?

最简单的方式就是使用反射,应对这种场景再适合不过了,但是接受数据量是很大的,使用反射就不得不考虑额外的性能开销。

查了很多资料最终参考了fastjson2再解决这种问题采用的思路,就是使用LambdaMetafactory

因此引出本文,特意记录并测试下几种方法调用方案的性能差异

实测

采用 普通调用反射MethodHandleLambdaMetafactorylambda几种方式,分别执行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的性能要比反射好很多,但最快的还是最后的两种方式:LambdaMetafactorylambda,和直接调用性能基本一致。

分析

反射

反射性能差是大名鼎鼎的,但这里不考虑获取Method的耗时,只是考虑invoke(实际调用方法)的性能。

反射是java1.1中纯java代码实现的,其中方法(包括invoke方法)本身逻辑复杂、分支判断多、目标不清晰。JIT看到的是一个超大的、复杂的、近乎黑盒的方法,无法JIT优化。

MethodHandle

MethodHandle是jdk1.7引入的,其诞生背景如下:

  1. Groovy/Scala 等动态语言跑在 JVM,需要高频动态调用,反射太慢;
  2. 准备做 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 构建,动态生成直接调用代码,性能最优,适合高频调用场景。实际开发中按需选型即可。

相关推荐
码上小翔哥1 小时前
Spring Boot Redis 缓存序列化踩坑记:GenericJackson2JsonRedisSerializer 的数组反序列化陷阱
java·redis
SamDeepThinking1 小时前
你认为从0-1开发一个项目最难的地方是什么?
java·后端·架构
Devin~Y1 小时前
大厂Java面试实战:Spring Boot/Cloud、Redis/Kafka、JVM调优与Spring AI RAG(内容社区UGC+AIGC客服场景)
java·jvm·spring boot·redis·spring cloud·kafka·mybatis
青山师1 小时前
CompletableFuture深度解析:异步编程范式与源码实现
java·单例模式·面试·性能优化·并发编程
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第42题】【JVM篇】第2题:JVM内存模型有哪些组成部分?
java·开发语言·jvm·面试
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第43题】【JVM篇】第3题:GC分为哪两种?Young GC 和 Full GC有什么区别?
java·开发语言·jvm·后端·面试
Carino_U1 小时前
并发编程之CPU缓存架构&Disruptor
java·缓存·架构
小雅痞2 小时前
[Java][Leetcode middle] 54. 螺旋矩阵
java·leetcode·矩阵
ooseabiscuit2 小时前
Laravel6.x新特性全解析
java·开发语言·后端·mysql·spring