平时写代码经常用到 lambda 表达式,在 mybatis-plus 的动态查询接口中,更是通过 lambda 方式来获取字段名。那么 lambda 到底是怎么实现的呢?mybatis-plus 又是怎么通过 lambda 获取到对应的字段名的?
要知道 lambda 具体怎么实现的,最直接的方式当然是看字节码了,idea 编译下,然后选中对应的类, 选择 View -> Show Bytecode
就可以查看字节码了。也可以敲命令行或者用 jclasslib 插件看,不过 idea 自带的看起来方便点。 这里直接把字节码贴出来方便点。
public class Demo {
public static void main(String[] args) {
Function<Object, String> os = Object::toString;
os.apply(new Object());
}
}
/// 字节码
// class version 52.0 (52)
// access flags 0x21
public class devtest/demo/utils/Demo {
// compiled from: Demo.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0x1
public <init>()V
L0
LINENUMBER 10 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Ldevtest/demo/utils/Demo; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
// parameter args
L0
LINENUMBER 13 L0
INVOKEDYNAMIC apply()Ljava/util/function/Function; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(Ljava/lang/Object;)Ljava/lang/Object;,
// handle kind 0x5 : INVOKEVIRTUAL
java/lang/Object.toString()Ljava/lang/String;,
(Ljava/lang/Object;)Ljava/lang/String;
]
ASTORE 1
L1
LINENUMBER 14 L1
ALOAD 1
NEW java/lang/Object
DUP
INVOKESPECIAL java/lang/Object.<init> ()V
INVOKEINTERFACE java/util/function/Function.apply (Ljava/lang/Object;)Ljava/lang/Object; (itf)
POP
L2
LINENUMBER 15 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE os Ljava/util/function/Function; L1 L3 1
// signature Ljava/util/function/Function;
// declaration: os extends java.util.function.Function
MAXSTACK = 3
MAXLOCALS = 2
}
可以看到 main 方法里面有一个 INVOKEDYNAMIC
的指令,后面跟了一个我们想实现的 Function 接口,以及 LambdaMetafactory.metafactory
方法。
大概简述一下其实现逻辑:INVOKEDYNAMIC
是 java7 新增的一个指令,可以实现动态调用的功能。它会将调用指向引导方法(bootstrap method, BSM)返回的调用点 CallSite
对象。CallSite
内部持有一个 MethodHandle
方法句柄 ,对应某个方法。
在这里,引导方法就对应 LambdaMetafactory.metafactory
,通过 debug 断点可以看到其执行流程:
然后主要来看下 LambdaMetafactory.metafactory
是如何生成调用点的:
/// 入口
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
/// 创建一个 内部类 lambda 元工厂,携带了 lambda 的实现方式信息
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
/// 创建一个 调用点
return mf.buildCallSite();
}
/// InnerClassLambdaMetafactory.java
/// 创建调用点的逻辑
CallSite buildCallSite() throws LambdaConversionException {
/// 这里会使用 asm 生成一个内部类
final Class<?> innerClass = spinInnerClass();
...
/// 加载生成的内部类
UNSAFE.ensureClassInitialized(innerClass);
/// invokedType 为 callsite 的期望签名,
if (invokedType.parameterCount() == 0) {
....
/// 无参的话,直接创建 lambda 内部类的实例, 例如 Object::toString 没有使用到具体实例
Object inst = ctrs[0].newInstance();
/// 这里 constant 返回的 methodhandle 大概是一个 inst -> inst 的方法句柄, 调用点最终返回这个内部类的实例
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
} else {
....
/// 存在参数的话,使用了实例对象, 如:SC s = System.out::println;
/// 这种写法,需要一个 System.out 对象,这个生成的内部类和上面那种是不一样的
/// 获取内部类中的 get$Lambda 静态方法的 方法句柄,并生成一个固定调用点
return new ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP
.findStatic(innerClass, NAME_FACTORY, invokedType));
}
}
}
最后再来看一下生成的内部类具体是什么样子:
添加jvm参数可导出生成的合成(synthetic)类:-Djdk.internal.lambda.dumpProxyClasses=
下面是生成的两种 lambda 内部类:
/// Function
// $FF: synthetic class
final class BMSTest$$Lambda$1 implements Function {
private BMSTest$$Lambda$1() {
}
@Hidden
public Object apply(Object var1) {
return var1.toString();
}
}
/// Consumer c = System.out::println;
// $FF: synthetic class
final class BMSTest$$Lambda$3 implements Consumer {
private final PrintStream arg$1;
private BMSTest$$Lambda$3(PrintStream var1) {
this.arg$1 = var1;
}
private static Consumer get$Lambda(PrintStream var0) {
return new BMSTest$$Lambda$3(var0);
}
@Hidden
public void accept(Object var1) {
this.arg$1.println((String)var1);
}
}
我们已经知道 lambda 是通过 LambdaMetafactory.metafactory
创建内部类生成 CallSite
实现的,那要怎么才能获取其引用方法呢? 答案就是 InnerClassLambdaMetafactory#spinInnerClass
生成内部类的逻辑里。
只要 lambda 实现的接口实现了 serializable
接口,生成的内部类中就会带上一个 writeReplace
方法(序列化时调用),里面会返回一个 SerializedLambda
对象,里面保存了我们的实现方法 println
。
直接看成品:
/// public interface SC extends Consumer, Serializable {}
/// SC s = System.out::println;
// $FF: synthetic class
final class BMSTest$$Lambda$4 implements SC {
private final PrintStream arg$1;
private BMSTest$$Lambda$4(PrintStream var1) {
this.arg$1 = var1;
}
private static SC get$Lambda(PrintStream var0) {
return new BMSTest$$Lambda$4(var0);
}
@Hidden
public void accept(Object var1) {
this.arg$1.println((String)var1);
}
private final Object writeReplace() {
return new SerializedLambda(BMSTest.class, "BMSTest$SC", "accept", "(Ljava/lang/Object;)V", 5, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", "(Ljava/lang/String;)V", new Object[]{this.arg$1});
}
}
可以看到里面确实有一个 writeReplace
方法,里面也有我们需要的信息。
那我们只需要通过反射拿到这个内部类的 writeReplace
方法,再调用一下就可以拿到其 SerializedLambda
对象了。
这里给个示例:
public class LambdaUtil {
public interface SC<T> extends Consumer<T>, Serializable {}
public static void main(String[] args) throws Exception {
SC<String> sc = System.out::println;
String m = getImplMethod(sc);
System.out.println(m);
}
public static String getImplMethod(SC<?> lambda) {
SerializedLambda serialize = serialize(lambda);
Objects.requireNonNull(serialize);
return serialize.getImplMethodName();
}
public static final Map<SC<?>, SerializedLambda> cache = new ConcurrentHashMap<>();
public static SerializedLambda serialize(SC<?> lambda) {
if (!lambda.getClass().isSynthetic()) {
throw new RuntimeException("not synthetic");
}
return cache.computeIfAbsent(lambda, k -> {
Method writeReplace = null;
try {
writeReplace = k.getClass().getDeclaredMethod("writeReplace");
writeReplace.setAccessible(true);
Object sl = writeReplace.invoke(k);
return (SerializedLambda) sl;
} catch (Exception e) {
e.printStackTrace();
}
return null;
});
}
}
mybatis-plus 通过引用方法获取字段名也是利用的 lambda 的 Serializable
特性,只不过 MP (3.4.2)是让 lambda 序列化了一遍,然后再反序列化拿到 SerializedLambda
. 不理解。。 大概有什么坑没注意到?
这里推荐几篇相关的文档