前提
笔者在下班空余时间想以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
的函数式接口并且调用它:
1 | java复制代码public class App { |
执行的DEBUG
信息如下:
这样就能获取到函数式接口实例在调用方法时候的调用点运行时信息,甚至连泛型参数擦除前的类型都能拿到,那么就可以衍生出很多技巧。例如:
1 | java复制代码public class ConditionApp { |
很多人会担心反射调用的性能,其实在高版本的JDK,反射性能已经大幅度优化,十分逼近直接调用的性能,更何况有些场景是少量反射调用场景,可以放心使用。
前面花大量篇幅展示了SerializedLambda
的功能和使用,接着看Lambda
表达式的序列化与反序列化:
1 | java复制代码public class SerializedLambdaApp { |
结果如下图:
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
实例
基于这两种方式可以分别编写例子,例如反射方式如下:
1 | java复制代码// 反射方式 |
序列化和反序列方式会稍微复杂,因为ObjectInputStream.readObject()
方法会最终回调SerializedLambda.readResolve()
方法,导致返回的结果是一个新模板类承载的Lambda
表达式实例,所以这里需要想办法中断这个调用提前返回结果,方案是构造一个和SerializedLambda
相似但是不存在readResolve()
方法的影子类型:
1 | java复制代码package cn.vlts; |
被遗忘的$deserializeLambda$
方法
前文提到,Lambda
表达式实例反序列化的时候会调用java.lang.invoke.SerializedLambda.readResolve()
方法,神奇的是,此方法源码如下:
1 | java复制代码private Object readResolve() throws ReflectiveOperationException { |
看起来就是”捕获类”中存在一个这样的静态方法:
1 | java复制代码class CapturingClass { |
可以尝试检索”捕获类”中的方法列表:
1 | java复制代码public class CapturingClassApp { |
果真是存在一个和之前提到的java.lang.invoke.SerializedLambda
注释描述一致的”捕获类”的SerializedLambda
实例转化为Lambda
表达式实例的方法,因为搜索多处地方都没发现此方法的踪迹,猜测$deserializeLambda$
是方法由VM
生成,并且只能通过反射的方法调用,算是一个隐藏得比较深的技巧。
小结
JDK
中的Lambda
表达式功能已经发布很多年了,想不到这么多年后的今天才弄清楚其序列化和反序列化方式,虽然这不是一个复杂的问题,但算是最近一段时间看到的比较有意思的一个知识点。
参考资料:
JDK11
源码Mybatis-Plus
相关源码
(本文完 e-a-20211127 c-2-d)
本文转载自: 掘金