前提
筆者在下班空餘時間想以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
表示式功能已經發布很多年了,想不到這麼多年後的今天才弄清楚其序列化和反序列化方式,雖然這不是一個複雜的問題,但算是最近一段時間看到的比較有意思的一個知識點。
參考資料:
JDK11
原始碼Mybatis-Plus
相關原始碼
(本文完 e-a-20211127 c-2-d)