前提

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

神奇的Lambda表达式序列化

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

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

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

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

最后看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信息如下:

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

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表达式序列化的原理,可以直接参考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方法

图形化的过程如下:

获取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)

JDK中Lambda表达式的序列化与SerializedLambda的巧妙使用的更多相关文章

  1. Java中lambda表达式详解

    原文地址:http://blog.laofu.online/2018/04/20/java-lambda/ 为什么使用lambda 在java中我们很容易将一个变量赋值,比如int a =0;int ...

  2. Java8中Lambda表达式的10个例子

    Java8中Lambda表达式的10个例子 例1 用Lambda表达式实现Runnable接口 //Before Java 8: new Thread(new Runnable() { @Overri ...

  3. VS编译环境中TBB配置和C++中lambda表达式

    TBB(Thread Building Blocks),线程构建模块,是由Intel公司开发的并行编程开发工具,提供了对Windows,Linux和OSX平台的支持. TBB for Windows ...

  4. Java8 Collections.sort()及Arrays.sort()中Lambda表达式及增强版Comparator的使用

    摘要:本文主要介绍Java8 中Arrays.sort()及Collections.sort()中Lambda表达式及增强版Comparator的使用. 不废话直接上代码 import com.goo ...

  5. JDK 8 中Lambda表达式的使用

    认识Lambda表达式 首先来引入一个示例 new Thread(new Runnable() {     @Override     public void run() {         Syst ...

  6. Java中Lambda表达式的使用

    简介(译者注:虽然看着很先进,其实Lambda表达式的本质只是一个"语法糖",由编译器推断并帮你转换包装为常规的代码,因此你可以使用更少的代码来实现同样的功能.本人建议不要乱用,因 ...

  7. Java中Lambda表达式的使用(转)

    https://www.cnblogs.com/franson-2016/p/5593080.html 简介(译者注:虽然看着很先进,其实Lambda表达式的本质只是一个"语法糖" ...

  8. Java中Lambda表达式基础及使用详解

    概述 Lambda 是JDK 8 的重要新特性.它允许把函数作为一个方法的参数(函数作为参数传递进方法中),使用 Lambda 表达式可以使代码变的更加简洁紧凑,使Java代码更加优雅. 标准格式 三 ...

  9. Java中Lambda表达式的进化之路

    Lambda表达式的进化之路 为什么要使用Lambda表达式 可以简洁代码,提高代码的可读性 可以避免匿名内部类定义过多导致逻辑紊乱 在原先实现接口抽象方法的时候,需要通过定义一个实现接口的外部类来实 ...

随机推荐

  1. CF49E Common ancestor(dp+dp+dp)

    纪念卡常把自己卡死的一次自闭模拟赛 QWQ 一开始看这个题,以为是个图论,仔细一想,貌似可以直接dp啊. 首先,因为规则只有从两个变为1个,貌似可以用类似区间\(dp\)的方式来\(check\)一段 ...

  2. 036—环境变量path

    day04 课堂笔记 1.开发第一个java程序:HelloWorld 1.1.程序写完以后,一定要ctrl+s进行保存 源代码若修改,需重新进行编译 1.2.编译阶段 怎么编译?使用什么命令?这个命 ...

  3. 【UE4 设计模式】简单工厂模式 Simple Factory Pattern

    概述 描述 又称为静态工厂方法 一般使用静态方法,根据参数的不同创建不同类的实例 套路 创建抽象产品类 : 创建具体产品类,继承抽象产品类: 创建工厂类,通过静态方法根据传入不同参数从而创建不同具体产 ...

  4. 【c++ Prime 学习笔记】第4章 表达式

    表达式由一个或多个运算对象组成,对表达式求值返回结果. 字面值和变量是最简单的表达式 把运算符和运算对象组合可得到复杂表达式. 4.1 基础 4.1.1 基本概念 一元运算符作用于一个对象,如取地址符 ...

  5. Linux上传下载神器之 lrzsz

    在开发的过程中,经常遇到 需要在 Linux 和 Windows 之间上传下载文件的情况 这时,一般都是使用 FTP 或者 WinSCP 工具进行上传下载, 虽然也能解决问题,但是这些工具需要在本地安 ...

  6. 使用registry搭建docker私服仓库

    使用registry搭建docker私服仓库 一.拉取 registry镜像 二.根据镜像启动一个容器 1.创建一个数据卷 2.启动容器 三.随机访问一个私服的接口,看是否可以返回数据 四.推送一个镜 ...

  7. Noip模拟61 2021.9.25

    T1 交通 考场上想了一个$NPC$.应该吧,是要求出图里面的所有可行的不重复欧拉路 无数种做法都无法解出,时间也都耗在这个上面的,于是就考的挺惨的 以后要是觉得当前思路不可做,就试着换一换思路,千万 ...

  8. 上午小测3 T1 括号序列 && luogu P5658 [CSP/S 2019 D1T2] 括号树 题解

    前 言: 一直很想写这道括号树..毕竟是在去年折磨了我4个小时的题.... 上午小测3 T1 括号序列 前言: 原来这题是个dp啊...这几天出了好几道dp,我都没看出来,我竟然折磨菜. 考试的时候先 ...

  9. noip模拟11

    T1 math 就挺水一小破题目,第一眼看好像不可做,看着看着突然发现假设x和y的最大公约数是gcd,那么kx%y一定是gcd的倍数, 然后想到可以把所有数字与k的gcd求出来,打一个完全背包,可是仔 ...

  10. GEOS使用记录

    由于需要计算GIS障碍物的缓冲区,所以研究了 一下GEOS库的使用,将使用的一些细节内容记录一下: 1.vs2010IDE无法编译较高版本的GEOS库,较高版本的库使用了更加高级的C++语法,如果想使 ...