前提

笔者在下班空余时间想以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. noip2017D1T3逛公园(拓扑图上dp,记忆化搜索)

    QWQ前几天才刚刚把这个D1T3写完 看着题解理解了很久,果然我还是太菜了QAQ 题目大意就是 给你一个n个点,m条边的图,保证1能到达n,求从1到n的 (设1到n的最短路长度是d)路径长度在[d,d ...

  2. 2020.3.21--ICPC训练联盟周赛Benelux Algorithm Programming Contest 2019

    A Appeal to the Audience 要想使得总和最大,就要使最大值被计算的次数最多.要想某个数被计算的多,就要使得它经过尽量多的节点.于是我们的目标就是找到 k 条从长到短的链,这些链互 ...

  3. nginx源码编译安装(详解)

    nginx编译安装 安装步骤: 官网下载合适的版本,建议选择稳定版本. 官网地址:https://nginx.org wget https://nginx.org/download/nginx-1.2 ...

  4. 如何访问位于内网的Ubuntu主机

    如何访问位于内网的Ubuntu主机 内网主机为Ubuntu桌面版 内网主机Ubuntu字符串界面版 SSH远程主机管理工具推荐 SSH远程文件访问工具推荐 如何访问位于内网的Ubuntu主机 内网主机 ...

  5. leetcode 5/300 最长回文子串 py

    目录 题目说明 方法一:动态规划--状态转移方程 方法二:优化中心扩展算法 题目说明 要看明白求得是什么,最长回文字串是指例如cababa中ababa是最长的,不是求回文的部分aba 方法一:动态规划 ...

  6. Coursera Deep Learning笔记 序列模型(二)NLP & Word Embeddings(自然语言处理与词嵌入)

    参考 1. Word Representation 之前介绍用词汇表表示单词,使用one-hot 向量表示词,缺点:它使每个词孤立起来,使得算法对相关词的泛化能力不强. 从上图可以看出相似的单词分布距 ...

  7. PinPoint单节点部署及客户端配置方法

    在一次做项目中,需要涉及全链路压测,为了更好定位链路中某一节点可能会出现的问题,在繁忙之余,快速部署及应用了该链路工具,分享给大家~ 话不多说,开始部署~ 一.环境配置1.1 获取需要的依赖包进入ho ...

  8. Alpha阶段初始任务分配

    项目 内容 这个作业属于哪个课程 2021春季软件工程(罗杰 任健) 这个作业的要求在哪里 团队项目-计划-Alpha阶段说明书 一.Alpha阶段总体规划 进行服务器相关部署 进行开发相关技术学习 ...

  9. Bzoj P2212 [Poi2011]Tree Rotations | 线段树合并

    题目链接 通过观察与思考,我们可以发现,交换一个结点的两棵子树,只对这两棵子树内的节点的逆序对个数有影响,对这两棵子树以外的节点是没有影响的.嗯,然后呢?(っ•̀ω•́)っ 然后,我们就可以对于每一个 ...

  10. UVA 10004 Bicoloring(DFS染色)

    题意: 给N个点构成的无环无向图,并且保证所有点对都是连通的. 给每个点染色,要么染成黑要么染成白.问是否存在染色方案使得所有有边相连的点对颜色一定不一样. 是输出 BICOLORABLE 否则输出 ...