JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

throwable 發表於 2021-11-27

前提

筆者在下班空餘時間想以Javassist為核心基於JDBC寫一套摒棄反射呼叫的輕量級的ORM框架,過程中有研讀mybatistk-mappermybatis-plusspring-boot-starter-jdbc的原始碼,其中發現了mybatis-plus中的LambdaQueryWrapper可以獲取當前呼叫的Lambda表示式中的方法資訊(實際上是CallSite的資訊),這裡做一個完整的記錄。本文基於JDK11編寫,其他版本的JDK不一定合適。

神奇的Lambda表示式序列化

之前在看Lambda表示式原始碼實現的時候沒有細看LambdaMetafactory的註釋,這個類頂部大量註釋中其中有一段如下:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

簡單翻譯一下就是:可序列化特性。一般情況下,生成的函式物件(這裡應該是特指基於Lambda表示式實現的特殊函式物件)不需要支援序列化特性。如果需要支援該特性,FLAG_SERIALIZABLELambdaMetafactory的一個靜態整型屬性,值為1 << 0)可以用來表示函式物件是序列化的。一旦使用了支援序列化特性的函式物件,那麼它們以SerializedLambda類的形式序列化,這些SerializedLambda例項需要額外的"捕獲類"的協助(捕獲類,如MethodHandles.Lookupcaller引數所描述),詳細資訊參閱SerializedLambda

LambdaMetafactory的註釋中再搜尋一下FLAG_SERIALIZABLE,可以看到這段註釋:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

大意為:設定了FLAG_SERIALIZABLE標記後生成的函式物件例項會實現Serializable介面,並且會存在一個名字為writeReplace的方法,該方法的返回值型別為SerializedLambda。呼叫這些函式物件的方法(前面提到的"捕獲類")的呼叫者必須存在一個名字為$deserializeLambda$的方法,如SerializedLambda類所描述。

最後看SerializedLambda的描述,註釋有四大段,這裡貼出並且每小段提取核心資訊:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

各個段落大意如下:

  • 段落一:SerializedLambdaLambda表示式的序列化形式,這類儲存了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資訊如下:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

這樣就能獲取到函式式介面例項在呼叫方法時候的呼叫點執行時資訊,甚至連泛型引數擦除前的型別都能拿到,那麼就可以衍生出很多技巧。例如:

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);
    }
}

結果如下圖:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

Lambda表示式序列化原理

關於Lambda表示式序列化的原理,可以直接參考ObjectStreamClassObjectOutputStreamObjectInputStream的原始碼,這裡直接說結論:

  • 前提條件:待序列化物件需要實現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方法

圖形化的過程如下:

JDK中Lambda表示式的序列化與SerializedLambda的巧妙使用

獲取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表示式功能已經發布很多年了,想不到這麼多年後的今天才弄清楚其序列化和反序列化方式,雖然這不是一個複雜的問題,但算是最近一段時間看到的比較有意思的一個知識點。

參考資料:

  • JDK11原始碼
  • Mybatis-Plus相關原始碼

(本文完 e-a-20211127 c-2-d)