JDK中Lambda表达式的序列化与SerializedLambda的巧妙使用

前提

笔者在下班空余时间想一想 Javassist 为核心基于 JDBC 写一套摒弃反射调用的轻量级的 ORM 框架,过程中有研读 mybatis 、 tk-mapper 、 mybatis-plus 和 spring-boot-starter-jdbc 的源代码,其中发现了 mybatis-plus 中的 LambdaQueryWrapper 可以获取当前调用的 Lambda 表达式中的方法信息(实际上是 CallSite 的信息),在这里做一个完整的记录。本文基于 JDK11 编写完,其他版本的 JDK 不一定合适。

神奇的Lambda表达式序列化

之前在看 Lambda 表达式源码实现的时候没有细看 LambdaMetafactory 的注释,这个类顶部大量注释中其中有一段如下:

简单翻译一下就是:可序列化特性。一般情况下,生成的函数对象(这里应该是特指基于 Lambda 表达式实现的特殊函数对象)不需要支持序列化特性。如果需要支持该特性, FLAG_SERIALIZABLE ( LambdaMetafactory 的一个静态整型属性,值为 1 << 0 )可以用来表示函数对象是序列化的。一旦使用了支持序列化特性的函数对象,那么它们以 SerializedLambda 类似的形式序列化,这些 SerializedLambda 实例需要额外的"捕获类"的协助(捕获类,如 MethodHandles.Lookup 的 caller 参数所描述),详细信息参阅 SerializedLambda 。

在 LambdaMetafactory 的注释中再搜索一下 FLAG_SERIALIZABLE ,可以看到这段注释:

大意为:设置了 FLAG_SERIALIZABLE 标记后生成的函数对象实例会实现 Serializable 接口,并且会存在一个名字为 writeReplace 的方法,该方法的返回值类型为 SerializedLambda 。调用这些函数对象的方法(前面提到的"捕获类")的调用者必须存在一个名字为 $deserializeLambda$ 的方法,如 SerializedLambda 类所描述。

最后看 SerializedLambda 的描述,注释有四大段,这里贴出并且每小段提取核心信息:

各个段落大意如下:

  • 段落一: SerializedLambda 是 Lambda 表达式的序列化形式,这类存储了 Lambda 表达式的运行时信息
  • 段落二:为了确保安全 Lambda 表达式的序列化实现正确性,编译器或者语言类库可以选用的一种方式是确保 writeReplace 方法返回一个 SerializedLambda 实例
  • 段落三: SerializedLambda 提供一个 readResolve 方法,其职能类似于调用"捕获类"中的静态方法 $deserializeLambda$(SerializedLambda) 并且把自身实例作为入参,该过程理解为反序列化过程
  • 段落四: 序列化和反序列化产生的函数对象的身份敏感操作的标识形式(如 System.identityHashCode() 、对象锁定等等)是不可预测的

最终的结论就是:如果一个函数式接口实现了 Serializable 接口,那么它的实例就会自动生成了一个返回 SerializedLambda 实例的 writeReplace 方法,可以从 SerializedLambda 实例中获取到这个函数式接口的运行时信息。这些运行时的信息就是 SerializedLambda 的属性:

属性

含义

capturingClass

"捕获类",当前的 Lambda 表达式出现的所在类

functionalInterfaceClass

名称,并且以"/"分隔,返回的 Lambda 对象的静态类型

functionalInterfaceMethodName

函数式接口方法名称

functionalInterfaceMethodSignature

函数式接口方法签名(其实是参数类型和返回值类型,如果使用了泛型则是擦除后的类型)

implClass

名称,并且以"/"分隔,持有该函数式接口方法的实现方法的类型(实现了函数式接口方法的实现类)

implMethodName

函数式接口方法的实现方法名称

implMethodSignature

函数式接口方法的实现方法的方法签名(是参数类型和返回值类型)

instantiatedMethodType

用实例类型变量替换后的函数式接口类型

capturedArgs

Lambda 捕获的动态参数

implMethodKind

实现方法的 MethodHandle 类型

举个实际的例子,定义一个实现了 Serializable 的函数式接口并且调用它:

public class App {

    @FunctionalInterface
    public interface CustomerFunction<S, T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        Long result = function.convert("123");
        System.out.println(result);
        Method method = function.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true);
        SerializedLambda serializedLambda = (SerializedLambda)method.invoke(function);
        System.out.println(serializedLambda.getCapturingClass());
    }
}

执行的 DEBUG 信息如下:

这样就能获取到函数式接口实例在调用方法时候的调用点运行时信息,甚至连泛型参数擦除前的类型都能拿到,那么就可以衍生出很多技巧。例如:

public class ConditionApp {

    @FunctionalInterface
    public interface CustomerFunction<S, T> extends Serializable {

        T convert(S source);
    }

    @Data
    public static class User {

        private String name;
        private String site;
    }

    public static void main(String[] args) throws Exception {
        Condition c1 = addCondition(User::getName, "=", "throwable");
        System.out.println("c1 = " + c1);
        Condition c2 = addCondition(User::getSite, "IN", "('throwx.cn','vlts.cn')");
        System.out.println("c1 = " + c2);
    }

    private static <S> Condition addCondition(CustomerFunction<S, String> function,
                                              String operation,
                                              Object value) throws Exception {
        Condition condition = new Condition();
        Method method = function.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(true);
        SerializedLambda serializedLambda = (SerializedLambda) method.invoke(function);
        String implMethodName = serializedLambda.getImplMethodName();
        int idx;
        if ((idx = implMethodName.lastIndexOf("get")) >= 0) {
            condition.setField(Character.toLowerCase(implMethodName.charAt(idx + 3)) + implMethodName.substring(idx + 4));
        }
        condition.setEntityKlass(Class.forName(serializedLambda.getImplClass().replace("/", ".")));
        condition.setOperation(operation);
        condition.setValue(value);
        return condition;
    }

    @Data
    private static class Condition {

        private Class<?> entityKlass;
        private String field;
        private String operation;
        private Object value;
    }
}

// 执行结果
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User, field=name, operation==, value=throwable)
c1 = ConditionApp.Condition(entityKlass=class club.throwable.lambda.ConditionApp$User, field=site, operation=IN, value=('throwx.cn','vlts.cn'))

很多人会担心反射调用的性能,其实在高版本的JDK,反射性能已经大幅度优化,十分逼近直接调用的性能,更何况有些场景是少量反射调用场景,可以放心使用。

前面花大量篇幅展示了 SerializedLambda 的功能和使用,接着看 Lambda 表达式的序列化与反序列化:

public class SerializedLambdaApp {

    @FunctionalInterface
    public interface CustomRunnable extends Serializable {

        void run();
    }

    public static void main(String[] args) throws Exception {
        invoke(() -> {
        });
    }

    private static void invoke(CustomRunnable customRunnable) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(customRunnable);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        Object target = ois.readObject();
        System.out.println(target);
    }
}

结果如下图:

Lambda表达式序列化原理

关于 Lambda 表达式序列化的原理,可以直接参考 ObjectStreamClass 、 ObjectOutputStream 和 ObjectInputStream 的源码,在这里直接说结论:

  • 前提条件:序列化对象需要实现 Serializable 接口
  • 待序列化对象中如果存在 writeReplace 方法,则直接基于传入的实例反射调用此方法得到的返回值类型作为序列化的目标类型,对于 Lambda 表达式就是 SerializedLambda 类型
  • 反序列化的过程刚好是逆转的过程,调用的方法为 readResolve ,刚好前面提到 SerializedLambda 也存在同名的私有方法
  • Lambda 表达式的实现类型是 VM 生成的模板类,从结果上观察,序列化前的实例和反序列化后得到的实例属于不同的模板类,对于前一小节的例子某次运行的结果中序列化前的模板类为 club.throwable.lambda.SerializedLambdaApp$$Lambda$14/0x0000000800065840 ,反序列化后的模板类为 club.throwable.lambda.SerializedLambdaApp$$Lambda$26/0x00000008000a4040

ObjectStreamClass是序列化和反序列化实现的类描述符,关于对象序列化和反序列化的类描述信息可以从这个类里面的成员属性找到,例如这里提到的writeReplace和readResolve方法

图形化的过程如下:

获取SerializedLambda的方式

通过前面的分析,得知有两种方式可以获取 Lambda 表达式的 SerializedLambda 实例:

  • 方式一:基于 Lambda 表达式实例和 Lambda 表达式的模板类反射调用 writeReplace 方法,得到的返回值就是 SerializedLambda 实例
  • 方式二:基于序列化和反序列化的方式获取 SerializedLambda 实例

基于这两种方式可以分别编写例子,例如反射方式如下:

// 反射方式
public class ReflectionSolution {

    @FunctionalInterface
    public interface CustomerFunction<S, T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        SerializedLambda serializedLambda = getSerializedLambda(function);
        System.out.println(serializedLambda.getCapturingClass());
    }

    public static SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
        Method writeReplaceMethod = serializable.getClass().getDeclaredMethod("writeReplace");
        writeReplaceMethod.setAccessible(true);
        return (SerializedLambda) writeReplaceMethod.invoke(serializable);
    }
}

序列化和反序列方式会稍微复杂,因为
ObjectInputStream.readObject() 方法会最终回调
SerializedLambda.readResolve() 方法,导致返回的结果是一个新模板类承载的 Lambda 表达式实例,所以这里需要想办法中断这个调用提前返回结果,方案是构造一个和 SerializedLambda 相似但是不存在 readResolve() 方法的 影子类型 :

package cn.vlts;
import java.io.Serializable;

/**
 * 这里注意一定要和java.lang.invoke.SerializedLambda同名,可以不同包名,这是为了"欺骗"ObjectStreamClass中有个神奇的类名称判断classNamesEqual()方法
 */
@SuppressWarnings("ALL")
public class SerializedLambda implements Serializable {
    private static final long serialVersionUID = 8025925345765570181L;
    private  Class<?> capturingClass;
    private  String functionalInterfaceClass;
    private  String functionalInterfaceMethodName;
    private  String functionalInterfaceMethodSignature;
    private  String implClass;
    private  String implMethodName;
    private  String implMethodSignature;
    private  int implMethodKind;
    private  String instantiatedMethodType;
    private  Object[] capturedArgs;

    public String getCapturingClass() {
        return capturingClass.getName().replace('.', '/');
    }
    public String getFunctionalInterfaceClass() {
        return functionalInterfaceClass;
    }
    public String getFunctionalInterfaceMethodName() {
        return functionalInterfaceMethodName;
    }
    public String getFunctionalInterfaceMethodSignature() {
        return functionalInterfaceMethodSignature;
    }
    public String getImplClass() {
        return implClass;
    }
    public String getImplMethodName() {
        return implMethodName;
    }
    public String getImplMethodSignature() {
        return implMethodSignature;
    }
    public int getImplMethodKind() {
        return implMethodKind;
    }
    public final String getInstantiatedMethodType() {
        return instantiatedMethodType;
    }
    public int getCapturedArgCount() {
        return capturedArgs.length;
    }
    public Object getCapturedArg(int i) {
        return capturedArgs[i];
    }
}


public class SerializationSolution {

    @FunctionalInterface
    public interface CustomerFunction<S, T> extends Serializable {

        T convert(S source);
    }

    public static void main(String[] args) throws Exception {
        CustomerFunction<String, Long> function = Long::parseLong;
        cn.vlts.SerializedLambda serializedLambda = getSerializedLambda(function);
        System.out.println(serializedLambda.getCapturingClass());
    }

    private static cn.vlts.SerializedLambda getSerializedLambda(Serializable serializable) throws Exception {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(serializable);
            oos.flush();
            try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
                @Override
                protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
                    Class<?> klass = super.resolveClass(desc);
                    return klass == java.lang.invoke.SerializedLambda.class ? cn.vlts.SerializedLambda.class : klass;
                }
            }) {
                return (cn.vlts.SerializedLambda) ois.readObject();
            }
        }
    }
}

被遗忘的$deserializeLambda$方法

前文提到, Lambda 表达式实例反序列化的时候会调用
java.lang.invoke.SerializedLambda.readResolve() 方法,神奇的是,此方法源码如下:

private Object readResolve() throws ReflectiveOperationException {
    try {
        Method deserialize = AccessController.doPrivileged(new PrivilegedExceptionAction<>() {
            @Override
            public Method run() throws Exception {
                Method m = capturingClass.getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
                m.setAccessible(true);
                return m;
            }
        });

        return deserialize.invoke(null, this);
    }
    catch (PrivilegedActionException e) {
        Exception cause = e.getException();
        if (cause instanceof ReflectiveOperationException)
            throw (ReflectiveOperationException) cause;
        else if (cause instanceof RuntimeException)
            throw (RuntimeException) cause;
        else
            throw new RuntimeException("Exception in SerializedLambda.readResolve", e);
    }
}

看起来就是"捕获类"中存在一个这样的静态方法:

class CapturingClass {

    private static Object $deserializeLambda$(SerializedLambda serializedLambda){
        return [serializedLambda] => Lambda表达式实例;
    }  
}

可以尝试检索"捕获类"中的方法列表:

public class CapturingClassApp {

    @FunctionalInterface
    public interface CustomRunnable extends Serializable {

        void run();
    }

    public static void main(String[] args) throws Exception {
        invoke(() -> {
        });
    }

    private static void invoke(CustomRunnable customRunnable) throws Exception {
        Method writeReplaceMethod = customRunnable.getClass().getDeclaredMethod("writeReplace");
        writeReplaceMethod.setAccessible(true);
        java.lang.invoke.SerializedLambda serializedLambda = (java.lang.invoke.SerializedLambda)
                writeReplaceMethod.invoke(customRunnable);
        Class<?> capturingClass = Class.forName(serializedLambda.getCapturingClass().replace("/", "."));
        ReflectionUtils.doWithMethods(capturingClass, method -> {
                    System.out.printf("方法名:%s,修饰符:%s,方法参数列表:%s,方法返回值类型:%s\n", method.getName(),
                            Modifier.toString(method.getModifiers()),
                            Arrays.toString(method.getParameterTypes()),
                            method.getReturnType().getName());
                },
                method -> Objects.equals(method.getName(), "$deserializeLambda$"));
    }
}

// 执行结果
方法名:$deserializeLambda$,修饰符:private static,方法参数列表:[class java.lang.invoke.SerializedLambda],方法返回值类型:java.lang.Object

果真是存在一个和之前提到的
java.lang.invoke.SerializedLambda 注释描述一致的"捕获类"的 SerializedLambda 实例转化为 Lambda 表达式实例的方法,因为搜索多处地方都没发现此方法的踪迹,猜测 $deserializeLambda$ 是方法由 VM 生成,并且只能通过反射的方法调用,算是一个隐藏得比较深的技巧。

小结

JDK 中的 Lambda 表达式功能已经发布很多年了,想不到这么多年后的今天才弄清楚其序列化和反序列化方式,虽然这不是一个复杂的问题,但算是最近一段时间看到的比较有意思的一个知识点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值