Lambda表達式獲取傳入的方法引用的方法名


MyBatisPlus的lambdaQuery,可以在構造查詢條件時傳遞方法的引用,MyBatis能夠將方法引用解析成爲要查詢的DB字段名,如下

Wrappers.<Member>lambdaQuery().eq(Member::getMemberId, memberId);
// where member_id = #{memberId}

如何做到的?首先根據已有知識推測一下,MyBatisPlus是根據PO屬性名轉化爲DB字段名的,這中間只要把變量的駝峯命名轉爲下劃線命名即可,所以得到PO屬性名就能得知DB字段名;而如果知道getMemberId這個方法名,就能夠得知字段名memberId。所以猜測這裏MyBatisPlus是得到了方法引用的方法名,然後推測出了字段名;

MyBatisPlus如何獲取方法引用的方法名

然後跟蹤lambdaQuery的eq方法調用鏈,如下:

LambdaQueryWrapperAbstractWrapperAbstractLambdaWrapperLambdaUtilsStringUtilseq(boolean, R, Object )addCondition(boolean, R, SqlKeyword, Object)columnToString(SFunction<T, ?>, boolean)resolve(SFunction<T, ?>)getColumn(SerializedLambda lambda)resolveFieldName(lambda.getImplMethodName())LambdaQueryWrapperAbstractWrapperAbstractLambdaWrapperLambdaUtilsStringUtils

在StringUtils.resolveFieldName可以看到如下代碼,可以證明MyBatisPlus確實是得到了方法引用的方法名,然後將方法名轉換爲字段名:

    public static String resolveFieldName(String getMethodName) {
        if (getMethodName.startsWith("get")) {
            getMethodName = getMethodName.substring(3);
        } else if (getMethodName.startsWith(IS)) {
            getMethodName = getMethodName.substring(2);
        }
        // 小寫第一個字母
        return StringUtils.firstToLowerCase(getMethodName);
    }

同時在getColumn中可以看到,這裏的方法名是通過SerializedLambda.getImplMethodName方法得到的。LambdaUtils.resolve(SFunction<T, ?> func) 返回一個SerializedLambda,這個SerializedLambda是Lambda表達式在序列化的時候,用來描述Lambda表達式信息的類,主要字段如下:

// lambda表達式所在外部類的類對象
private final Class<?> capturingClass;
// lambda表達式代替的函數式接口
private final String functionalInterfaceClass;
// lambda表達式代替的函數
private final String functionalInterfaceMethodName;
// lambda表達式代替函數的簽名,是這種形式:(Ljava/lang/Object;)V
private final String functionalInterfaceMethodSignature;
// lambda表達式執行時,實際執行的是一個方法,這個屬性是實際執行方法所在類的類名,如:java/io/PrintStream
private final String implClass;
// lambda表達式執行時,執行的方法名稱,如:println
private final String implMethodName;
// lambda表達式執行時,執行的方法的簽名:如:(Ljava/lang/Object;)V
private final String implMethodSignature;
// lambda表達式執行時,動態調用通過MethodHandle實現,這裏是MethodHandle在JVM層次引用的指令類型;具體見MethodHandleInfo;
private final int implMethodKind;
// lambda表達式代替的函數式接口如果存在泛型,則這個屬性是泛型在lambda中實際應用的類型的簽名,如:(Ljava/lang/Long;)V
private final String instantiatedMethodType;
// MethodHandle調用時的動態參數
private final Object[] capturedArgs;

字段較多,但目的是獲取方法引用的方法名,所以只需要用到implMethodName一個字段就行;源碼跟蹤到現在,得到一個結論:得到SerializedLambda,就能得知Lambda表達式中方法引用的方法名。

MyBatisPlus如何得到SerializedLambda

既然得到了SerializedLambda就能得知引用的方法名,那麼重點就轉移到了如何獲取SerializedLambda上。
繼續跟蹤MyBatisPlus源碼,發現獲取的方式在com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda.resolve()中;這裏有一個要注意的地方,我們前面跟蹤代碼的時候,使用到的是com.baomidou.mybatisplus.core.toolkit.support包下的SerializedLambda,同時在java.lang.invoke包下也有一個SerializedLambda,這兩個類中的字段名一樣,方法基本相同(都是些getter和setter),具體爲什麼MyBatisPlus要再寫一個同名類,下面具體分析,先來分析代碼:

public static SerializedLambda resolve(SFunction lambda) {
    // isSynthetic返回值代表對象是否是一個自動生成的類,lambda、匿名內部類都屬於自動生成的類;
    if (!lambda.getClass().isSynthetic()) {
        throw ExceptionUtils.mpe("該方法僅能傳入 lambda 表達式產生的合成類");
    }
    // 這裏將lambda表達式序列化寫入ObjectInputStream,然後再反序列化回來,得到了SerializedLambda(readResolve)
    try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(SerializationUtils.serialize(lambda))) {
        // 先說說這個方法的作用;resolveClass是在反序列化對象時,決定得到的對象的類型;
        // 因爲在序列化一個對象的時候,實際寫到流中的對象數據,可能並不是被序列化的對象的類型,而是一個其他類型的對象;(writeReplace)
        // 所以反序列化對象的時候,也需要根據對象流中的描述信息,來決定反序列化爲一個什麼類型的對象
        @Override
        protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
            // ObjectStreamClass 是對象流中數據的描述信息
            // 如果對象流中的數據是jdk的SerializedLambda類型,則將對象反序列化爲MyBatisPlus聲明的SerializedLambda;否則按照對象流中的類型反序列化;
            Class<?> clazz = super.resolveClass(objectStreamClass);
            return clazz == java.lang.invoke.SerializedLambda.class ? SerializedLambda.class : clazz;
        }
    }) {
        return (SerializedLambda) objIn.readObject();
    } catch (ClassNotFoundException | IOException e) {
        throw ExceptionUtils.mpe("This is impossible to happen", e);
    }
}

代碼分析中有兩個疑問:

  1. 爲什麼MyBatisPlus工具中,lambda序列化後,反序列化回來不是一個lambda表達式?
  2. 爲什麼序列化對象到對象流時,寫入到流的對象類型和實際需要序列化的對象類型可能不同?

爲了解決這兩個疑問,需要簡單的瞭解一下對象序列化中的 writeReplace 和 readResolve:

  • writeReplace:在將對象序列化之前,如果對象的類或父類中存在writeReplace方法,則使用writeReplace的返回值作爲真實被序列化的對象;writeReplace在writeObject之前執行;

  • readResolve:在將對象反序列化之後,ObjectInputStream.readObject返回之前,如果從對象流中反序列化得到的對象所屬類或父類中存在readResolve方法,則使用readResolve的返回值作爲ObjectInputStream.readObject的返回值;readResolve在readObject之後執行;

    函數式接口如果繼承了Serializable,使用Lambda表達式來傳遞函數式接口時,編譯器會爲Lambda表達式生成一個writeReplace方法,這個生成的writeReplace方法會返回java.lang.invoke.SerializedLambda;可以從反射Lambda表達式的Class證明writeReplace的存在(具體操作與截圖在後面);所以在序列化Lambda表達式時,實際上寫入對象流中的是一個SerializedLambda對象,且這個對象包含了Lambda表達式的一些描述信息
    SerializedLambda類中有readResolve方法,這個readResolve方法中通過反射調用了Lambda表達式所在外部類中的**$deserializeLambda$**方法,這個方法是編譯器自動生成的,可以通過反編譯.class字節碼證明(具體操作與截圖在後面);$deserializeLambda$方法內部解析SerializedLambda,並調用LambdaMetafactory.altMetafactory或LambdaMetafactory.metafactory方法(引導方法)得到一個調用點(CallSite),CallSite會被動態指定爲Lambda表達式代表的函數式接口類型,並作爲Lambda表達式返回;所以在從對象流反序列化得到SerializedLambda對象之後,又被轉換成原來的Lambda表達式,通過ObjectInputStream.readObject返回

如此,可以解答上面的兩個疑問:

  1. 爲什麼MyBatisPlus工具中,lambda序列化後,反序列化回來不是一個Lambda表達式?
    正常的Lambda表達式序列化再反序列化,得到的還是一個Lambda表達式;
    但在MyBatisPlus中,反序列化對象流之前,MyBatisPlus使用自己聲明的com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda來代替JDK中的java.lang.invoke.SerializedLambda;而MyBatisPlus聲明的SerializedLambda中沒有readResolve方法,所以readObject的返回值是代表了Lambda表達式信息的com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda對象,而不是一個Lambda表達式;
    MyBatisPlus使用自己聲明的SerializedLambda來反序列化對象沒有報錯,是因爲它與JDK中的SerializedLambda有相同的serialVersionUID和字段名,反序列化時會認爲是正確的類,具體內容就不探討了。
  2. 爲什麼序列化對象到對象流時,寫入到流的對象類型和實際需要序列化的對象類型可能不同?
    因爲writeReplace機制的存在,序列化Lambda表達式時,實際寫入對象流的,是含有Lambda表達式描述信息的SerializedLambda,而不是具體的Lambda表達式;

更簡單的得到SerializedLambda的方法

我們的目的是得到SerializedLambda,看一下上面的流程,如果函數式接口實現了Serializable,在Lambda表達式編譯時生成的writeReplace方法不就能直接得到SerializedLambda?上手測試一番:
首先聲明函數式接口,因爲函數式接口必須實現Serializable,所以沒有用JDK自帶的幾個:

public interface SerializableConsumer<T> extends Serializable {
    void accept(T t);
}

再寫一個調用Lambda表達式的方法:

public class LambdaTest {
    public static void main(String[] args) throws Exception {
        doConsume(System.out::println);
    }

    private static void doConsume(SerializableConsumer<Long> consumer) throws Exception {
        consumer.accept(123L);
        // 直接調用writeReplace
        Method writeReplace = consumer.getClass().getDeclaredMethod("writeReplace");
        writeReplace.setAccessible(true);
        Object sl = writeReplace.invoke(consumer);
        SerializedLambda serializedLambda = (SerializedLambda) sl;
        System.out.println(serializedLambda);
    }
}

可以看到,使用方法引用聲明的Lambda表達式,在編譯後是存在writeReplace方法的,而且返回值確實是SerializedLambda類型:
在這裏插入圖片描述
在這裏插入圖片描述
確認可以直接反射調用writeReplace得到(肯定可以啊,JDK裏都是這樣用的),就不需要像MyBatisPlus中那樣序列化再反序列化一次了。

另外還是再試一下序列化的方式,寫一個測試方法:

    public static void main(String[] args) throws Exception {
        doConsumeWithSerialize(System.out::println);
    }
    
    private static void doConsumeWithSerialize(SerializableConsumer<Long> consumer) throws Exception {
        consumer.accept(123L);
        // 先序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(consumer);
        // 再反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        SerializableConsumer<Long> newConsumer = (SerializableConsumer<Long>) ois.readObject();
        newConsumer.accept(234L);
    }

上面這個測試方法在序列化時報錯,因爲傳遞的方法引用是System.out::println,是一個特定實例方法引用,實例方法引用在調用時需要知道實例對象,因此序列化時會將System.out放到SerializedLambda的capturedArgs中(MethodHandle調用時需要的動態參數)。而System.out是PrintStream類型,沒有實現Serializable接口,所以序列化SerializedLambda時報錯。
這裏改一下代碼,使用靜態方法引用:

    public static void printIt(Long l) {
        System.out.println(l);
    }
    
    public static void main(String[] args) throws Exception {
        doConsumeWithSerialize(LambdaTest::printIt);
    }
    
    private static void doConsumeWithSerialize(SerializableConsumer<Long> consumer) throws Exception {
        consumer.accept(123L);
        // 先序列化
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(consumer);
        // 再反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
        SerializableConsumer<Long> newConsumer = (SerializableConsumer<Long>) ois.readObject();
        newConsumer.accept(234L);
    }

反編譯一下LambdaTest.class,看到確實有$deserializeLambda$方法;
在這裏插入圖片描述
$deserializeLambda$方法內部是一系列switch和if判斷,判斷SerializedLambda中的信息是不是在LambdaTest類中使用到的可序列化的函數式接口,如果是就通過引導方法;在返回語句中,調用了引導方法#0,將調用點與SerializableConsumer接口綁定,返回一個SerializableConsumer類型的實例;
在這裏插入圖片描述
引導方法#0如下:
在這裏插入圖片描述

總結

  1. 要得到Lambda表達式中方法引用的方法名,目前已知的方式是通過SerializedLambda
  2. SerializedLambda是對Lambda表達式進行描述的對象,在Lambda表達式可序列化的時候(函數式接口繼承Serializable)才能得到;
  3. 函數式接口繼承Serializable時,編譯器在編譯Lambda表達式時,生成了一個writeReplace方法,這個方法會返回SerializedLambda,可以反射調用這個方法;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章