最近想给orm工具包增加Lambda支持,实现User:getId映射到表id字段,碰到了问题,进行总结。

匿名内部类

java7的时候,有时候为了方便都使用内部类来实现部分代码

函数式接口定义

// @FunctionalInterface 不加也行,会自动推导
public interface MyFunction<T, R> {
    R apply(T t);
}

匿名内部类使用

public class MyTest {

    public static  <T, R> void execute(MyFunction<T, R> f, T a) {
        f.apply(a);
    }

    public static void main(String[] args) {
        MyTest.execute(new MyFunction<User, String>() {
            @Override
            public String apply(User user) {
                return user.getName();
            }
        }, new User());
    }
}

编译一下

javac *.java

反编译

> $ javap -c -p MyTest
public class MyTest {
   // ......
   public static <T, R> void execute(MyFunction<T, R>, T);
    Code:
       0: aload_0
       1: aload_1
       2: invokeinterface #2,  2            // InterfaceMethod MyFunction.apply:(Ljava/lang/Object;)Ljava/lang/Object;
       7: pop
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class MyTest$1
       3: dup
       4: invokespecial #4                  // Method MyTest$1."<init>":()V
       7: new           #5                  // class User
      10: dup
      11: invokespecial #6                  // Method User."<init>":()V
      14: invokestatic  #7                  // Method execute:(LMyFunction;Ljava/lang/Object;)V
      17: return
}

在main函数中看到创建了匿名内部类MyTest$1

> $ javap -c -p MyTest\$1
final class MyTest$1 implements MyFunction<User, java.lang.String> {
  // ......
  public java.lang.String apply(User);
    Code:
       0: aload_1
       1: invokevirtual #2                  // Method User.getName:()Ljava/lang/String;
       4: areturn

  public java.lang.Object apply(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #3                  // class User
       5: invokevirtual #4                  // Method apply:(LUser;)Ljava/lang/String;
       8: areturn
}

Lambda的使用

“Lambda 表达式”(lambda expression)是一个匿名函数,其基于数学中的λ演算得名。不像js可以var a = function,由于java中不能脱离类,通常以一个匿名内部类+内部一个方法构成的。

使用Lambda简化匿名内部类使用

// 修改main方法代码如下
MyTest.execute(User::getName, new User());

重新编译

javac MyTest.java MyFunction.java  User.java

看看字节码

> $ javap -v -p MyTest
......
Constant pool:
	// ......
    #3 = InvokeDynamic      #0:#29         // #0:apply:()LMyFunction;
	//......
{
  // ......

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: invokedynamic #3,  0              // InvokeDynamic #0:apply:()LMyFunction;
         5: new           #4                  // class User
         8: dup
         9: invokespecial #5                  // Method User."<init>":()V
        12: invokestatic  #6                  // Method execute:(LMyFunction;Ljava/lang/Object;)V
        15: return
      LineNumberTable:
        line 8: 0
        line 9: 15
}
SourceFile: "MyTest.java"
InnerClasses:
     public static final #47= #46 of #52; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #25 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;
    Method arguments:
      #26 (Ljava/lang/Object;)Ljava/lang/Object;
      #27 invokevirtual User.getName:()Ljava/lang/String;
      #28 (LUser;)Ljava/lang/String;

从结果来看会调用LambdaMetafactory.metafactory -> buildCallSite() -> spinInnerClass()。spinInnerClass此方法在内存创建类

使用 java -Djdk.internal.lambda.dumpProxyClasses MyTest 会看到生成 MyTest$$Lambda$1.class

> $ javap -p MyTest\$\$Lambda\$1
final class MyTest$$Lambda$1 implements MyFunction {
  private MyTest$$Lambda$1();
  public java.lang.Object apply(java.lang.Object);
}

Lambda序列化

实现Serializable接口

MfTest$$Lambda$1可以知道,此类未实现Serializable无法序列化,所以修改MyFunction如下

public interface MyFunction<T, R> extends Serializable {
    R apply(T t);
}

加上继承序列化后查看

final class MyTest$$Lambda$1 implements MyFunction {
	......
    private final Object writeReplace() {
        return new SerializedLambda(MyTest.class, "MyFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "User", "getName", "()Ljava/lang/String;", "(LUser;)Ljava/lang/String;", new Object[0]);
    }
}

可以看到实现了Serializable接口会增加writeReplace方法。

// 这个时候反编译MfTest结果如下
public class MyTest {
	// ......

  private static java.lang.Object $deserializeLambda$(java.lang.invoke.SerializedLambda);
    // .......
}

可以看到这里多了$deserializeLambda$方法。从SerializedLambda类上的注释可以了解到,SerializedLambda为可序列化lambdas的实现者,可以序列化的lambda类实现了writeReplace方法返回SerializedLambda的一个实例。
SerializedLambda有一个readResolve方法,它在捕获类中寻找名为Java BigDecimal 序列化 保留指定位数 java lambda 序列化_User(SerializedLambda)的静态方法,将其作为第一个参数调用,并返回结果。

获取映射属性

打印Lambda类所有方法

MyFunction<User, String> func = User::getName;
Class lambdaClass = func.getClass();
//打印类名:
System.out.print("类名:");
System.out.println(lambdaClass.getName());
//打印接口名:
System.out.print("接口名:");
Arrays.stream(lambdaClass.getInterfaces()).forEach(System.out::print);
System.out.println();
//打印方法名:
System.out.println("方法名:");
for (Method method : lambdaClass.getDeclaredMethods()) {
    System.out.println(method.getName() + "  ");
}

输出如下

类名:lambda.STest$$Lambda$1/999661724
接口名:interface lambda.MyFunction
方法名:
apply  
writeReplace

从这里没有突破口

SerializedLambda属性

public final class SerializedLambda implements Serializable {
    private static final long serialVersionUID = 8025925345765570181L;
    // lambda表达式出现的类
    private final Class<?> capturingClass;
    // 返回lambda对象的静态类型的名称,斜杠分隔形式
    private final String functionalInterfaceClass;
    // lambda工厂站点当前的函数接口方法的名称
    private final String functionalInterfaceMethodName;
    // 在lambda工厂站点上显示的函数接口方法的签名
    private final String functionalInterfaceMethodSignature;
    // 用斜杠分隔的形式表示,用于存放实现方法的类
    private final String implClass;
    // 实现方法的名称
    private final String implMethodName;
    // 实现方法的签名
    private final String implMethodSignature;
    // 方法处理实现方法的kind
    private final int implMethodKind;
    // 在类型变量被它们从捕获站点的实例化替换后,主功能接口方法的签名
    private final String instantiatedMethodType;
    // lambda工厂站点的动态参数,表示由lambda捕获的变量
    private final Object[] capturedArgs;

    public SerializedLambda(Class<?> capturingClass,
                            String functionalInterfaceClass,
                            String functionalInterfaceMethodName,
                            String functionalInterfaceMethodSignature,
                            int implMethodKind,
                            String implClass,
                            String implMethodName,
                            String implMethodSignature,
                            String instantiatedMethodType,
                            Object[] capturedArgs) {
        ......
    }
}

从writeReplace的代码可以知道,我们要找的其实是getName这个方法名,只要能获取到这个方法,我们就可以获取到对应的属性名称。从这个入参可以知道对应的是implMethodName属性,所以我们需要获取SerializedLambda再调用getImplMethodName获取对应的方法名称。

return new SerializedLambda(MyTest.class, "MyFunction", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", 5, "User", "getName", "()Ljava/lang/String;", "(LUser;)Ljava/lang/String;", new Object[0]);

获取SerializedLambda实例

反射方式获取

MyFunction<User, String> func = User::getName;
Class lambdaClass = func.getClass();
Method method = lambdaClass.getDeclaredMethod("writeReplace");
method.setAccessible(Boolean.TRUE);
SerializedLambda serializedLambda = (SerializedLambda) method.invoke(func);
String getterMethod = serializedLambda.getImplMethodName();
System.out.println("lambda表达式调用的方法名:" + getterMethod);
String fieldName = Introspector.decapitalize(getterMethod.replace("get", ""));
System.out.println("根据方法名得到的字段名:" + fieldName);

// 输出
lambda表达式调用的方法名:getName
根据方法名得到的字段名:name

序列化/反序列化获取

整体流程可参考下图

Java BigDecimal 序列化 保留指定位数 java lambda 序列化_java_02

MyFunction<User, String> func = User::getName;
Class<?> clazz = func.getClass();
// 将流转为 SerializedLambda 对象
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    oos.writeObject(func);
    oos.flush();
} catch (IOException ex) {
    throw new IllegalArgumentException("Failed to serialize object of type: " + clazz, ex);
}

try (ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())) {
    @Override
    protected Class<?> resolveClass(ObjectStreamClass objectStreamClass) throws IOException, ClassNotFoundException {
        Class<?> clazz;
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        try {
            clazz = Class.forName(objectStreamClass.getName(), false, cl);
        } catch (Exception ex) {
            clazz = super.resolveClass(objectStreamClass);
        }
        return clazz;
    }
}) {
    SerializedLambda lambda = (SerializedLambda) objIn.readObject();
    System.out.println(lambda.getImplMethodName());
} catch (ClassNotFoundException | IOException e) {
    throw new IllegalArgumentException("This is impossible to happen", e);
}

// 输出
Exception in thread "main" java.lang.ClassCastException: lambda.STest$$Lambda$4/114935352 cannot be cast to lambda.SerializedLambda

执行上面代码会报类型无法转换的问题。这是由于ObjectInputStream.readObject()方法会最终回调SerializedLambda.readResolve()方法,导致返回的结果是一个实现Serlializable的Lambda表达式实例,在实际操作的时候无法转成SerializedLambda实例。当前获取到的实例如下

Java BigDecimal 序列化 保留指定位数 java lambda 序列化_开发语言_03

所以这里需要中断这个调用提前返回结果,参考Mybatis plus的实现方式,我们拷贝SerializedLambda定义一个新的类,然后删除readResolve()方法。

// 修改代码
return clazz == java.lang.invoke.SerializedLambda.class ? lambda.SerializedLambda.class : clazz;

// 输出
getName

Java BigDecimal 序列化 保留指定位数 java lambda 序列化_开发语言_04


参考文档:

  1. JDK中Lambda表达式的序列化与SerializedLambda的巧妙使用
  2. Lambda表达式实现方式
  3. Mybatis-plus 源码