通过Lambda方法引用获取字段信息
最近项目中使用了MybatisPlus,在使用过程中看到MybatisPlus中的LambdaQueryWrapper可以通过Lambda表达式中的方法引用获取到字段名称:
//通过列名添加查询列
QueryWrapper<QywebInvoice> wrapper = new QueryWrapper<>();
wrapper.eq("uuid", orderUUID);
//通过lambda方法引用添加查询列
LambdaQueryWrapper<QywebInvoice> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(QywebInvoice::getOrderUuid, orderUUID)
.eq(QywebInvoice::getKpjgLp, FpkjztEnum.FPKJZT_DKJ.getType());于是很好奇底层实现的原理便研究了一番。
一开始我也定义一个函数式接口:
@FunctionalInterface
public interface SFunction<T, R> {
R apply(T t);
}尝试获取Student类的字段信息:
@Data
public class Student {
private String name;
}public class Test {
public static <T, R> String t(SFunction<T, R> function) {
Class<? extends SFunction> aClass = function.getClass();
System.out.println(aClass);
Method[] methods = aClass.getMethods();
String string = Arrays.toString(methods);
System.out.println(string);
return "hello";
}
public static void main(String[] args) {
Test.t(Student::getName);
}
}通过以上调用发现并不能获取到有关Student::getName任何信息:
class com.test.Test$$Lambda$1/1607521710
[public java.lang.Object com.test.Test$$Lambda$1/1607521710.apply(java.lang.Object), public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public java.lang.String java.lang.Object.toString(), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll()]于是进一步研究LambdaQueryWrapper的源码,发现MybatisPlus中的SFunction继承了序列化接口:

并且通过Lambad获取字段信息的关键在于:

这时我就觉得很奇怪,SFunction没有writeReplace方法,这个方法是怎么来的,又是如何通过这个方法获取到字段信息的?
我想到Lambda的方法引用本质不就是匿名内部类吗:
Test.t(new SFunction<Student, Object>() {
@Override
public Object apply(Student o) {
return o.getName();
}
});
/** 等价于 **/
Test.t(Student::getName);通过编译Test我们可以看到匿名内部类Test$1.class:
package com.test;
final class Test$1 implements SFunction<Student, Object> {
Test$1() {
}
public Object apply(Student o) {
return o.getName();
}
}于是想着Lambda是否也能编译出单独类似匿名内部类一样的class文件,通过编译发现lambda不能直接编译成类文件,需要加-Djdk.internal.lambda.dumpProxyClasses=D:\tmp:

再次运行在我指定的D:\tmp发现了被编译后的Lambdaclass文件Test$$Lambda$1.class:
package com.test;
import java.lang.invoke.LambdaForm.Hidden;
// $FF: synthetic class
final class Test$$Lambda$1 implements SFunction {
private Test$$Lambda$1() {
}
@Hidden
public Object apply(Object var1) {
return ((Student)var1).getName();
}
}我们可以看到Student::getName()的class文件跟new SFunction<Student, Object>()匿名内部类简直一模一样;但是我们并没有发现writeReplace()的存在;
于是依葫芦画瓢,我也跟着MybatisPlus学在自己的SFunction也继承序列化接口:
@FunctionalInterface
public interface SFunction<T, R> extends Serializable{
R apply(T t);
}
再把Test文件重新编译一次:
final class Test$$Lambda$1 implements SFunction {
private Test$$Lambda$1() {
}
@Hidden
public Object apply(Object var1) {
return ((Student)var1).getName();
}
private final Object writeReplace() {
return new SerializedLambda(Test.class, "com/test/SFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "com/test/Student", "getName", "()Ljava/lang/String;", "(Lcom/test/Student;)Ljava/lang/String;", new Object[0]);
}
}我们看到居然多个一个writeReplace方法,此方法会返回SerializedLambda对象,研究此类的构造器:
public SerializedLambda(Class<?> capturingClass,
String functionalInterfaceClass,
String functionalInterfaceMethodName,
String functionalInterfaceMethodSignature,
int implMethodKind,
String implClass,
String implMethodName,
String implMethodSignature,
String instantiatedMethodType,
Object[] capturedArgs) {
this.capturingClass = capturingClass;
this.functionalInterfaceClass = functionalInterfaceClass;
this.functionalInterfaceMethodName = functionalInterfaceMethodName;
this.functionalInterfaceMethodSignature = functionalInterfaceMethodSignature;
this.implMethodKind = implMethodKind;
this.implClass = implClass;
this.implMethodName = implMethodName;
this.implMethodSignature = implMethodSignature;
this.instantiatedMethodType = instantiatedMethodType;
this.capturedArgs = Objects.requireNonNull(capturedArgs).clone();
}我们能发现记录了Lambda方法引用的所有信息。
至此才知道原来函数式接口实现序列化后在被编译时会产生writeReplace();而我们通过反射执行该方法能过得到一个SerializedLambda对象,该对象能获取到函数式接口被Lambda方式调用的详细数据。
通过以上原理我们可以编写一个通过Lambda方法引用获取字段的工具类:
/**
* 将bean的属性的get/set方法,作为lambda表达式传入时,获取get/set方法对应的属性Field
*
* @param fn lambda表达式,bean的属性的get方法
* @param <T> 泛型
* @return 属性对象
*/
public static <T> Field getField(SFunction<T, ?> fn) {
// 从function取出序列化方法
Method writeReplaceMethod;
try {
writeReplaceMethod = fn.getClass().getDeclaredMethod("writeReplace");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
// 从序列化方法取出序列化的lambda信息
boolean isAccessible = writeReplaceMethod.isAccessible();
writeReplaceMethod.setAccessible(true);
SerializedLambda serializedLambda;
try {
serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(fn);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
writeReplaceMethod.setAccessible(isAccessible);
// 从lambda信息取出method、field、class等
String implMethodName = serializedLambda.getImplMethodName();
// 确保方法是符合规范的get/set方法,boolean类型是is开头
if (!implMethodName.startsWith("is") && !implMethodName.startsWith("get") && !implMethodName.startsWith("set")) {
throw new RuntimeException("get方法名称: " + implMethodName + ", 不符合java bean规范");
}
// get方法开头为 is 或者 get,将方法名 去除is或者get,然后首字母小写,就是属性名
int prefixLen = implMethodName.startsWith("is") ? 2 : 3;
String fieldName = implMethodName.substring(prefixLen);
String firstChar = fieldName.substring(0, 1);
fieldName = fieldName.replaceFirst(firstChar, firstChar.toLowerCase());
Field field;
try {
field = Class.forName(serializedLambda.getImplClass().replace("/", ".")).getDeclaredField(fieldName);
} catch (ClassNotFoundException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
return field;
}