lambda 实现原理及实现获取引用方法

12 篇文章 0 订阅

平时写代码经常用到 lambda 表达式,在 mybatis-plus 的动态查询接口中,更是通过 lambda 方式来获取字段名。那么 lambda 到底是怎么实现的呢?mybatis-plus 又是怎么通过 lambda 获取到对应的字段名的?

先来看看 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<Ljava/lang/Object;Ljava/lang/String;>;
    // declaration: os extends java.util.function.Function<java.lang.Object, java.lang.String>
    MAXSTACK = 3
    MAXLOCALS = 2
}

可以看到 main 方法里面有一个 INVOKEDYNAMIC 的指令,后面跟了一个我们想实现的 Function 接口,以及 LambdaMetafactory.metafactory 方法。

大概简述一下其实现逻辑:INVOKEDYNAMIC 是 java7 新增的一个指令,可以实现动态调用的功能。它会将调用指向引导方法(bootstrap method, BSM)返回的调用点 CallSite 对象。CallSite 内部持有一个 MethodHandle 方法句柄 ,对应某个方法。

在这里,引导方法就对应 LambdaMetafactory.metafactory ,通过 debug 断点可以看到其执行流程:
在这里插入图片描述

LambdaMetafactory.metafactory 执行流程

然后主要来看下 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<String> s = System.out::println; 
      		///      这种写法,需要一个 System.out 对象,这个生成的内部类和上面那种是不一样的
	      	/// 获取内部类中的 get$Lambda 静态方法的 方法句柄,并生成一个固定调用点
    	    return new ConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP
        	 	.findStatic(innerClass, NAME_FACTORY, invokedType));
         	}
        } 
    }

查看 lambda 合成类

最后再来看一下生成的内部类具体是什么样子:

添加jvm参数可导出生成的合成(synthetic)类:-Djdk.internal.lambda.dumpProxyClasses=<path>

下面是生成的两种 lambda 内部类:

/// Function<Object, String> os = Object::toString;

// $FF: synthetic class
final class BMSTest$$Lambda$1 implements Function {
    private BMSTest$$Lambda$1() {
    }

    @Hidden
    public Object apply(Object var1) {
        return var1.toString();
    }
}


/// Consumer<String> 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<T> extends Consumer<T>, Serializable {}
///  SC<String> 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. 不理解。。 大概有什么坑没注意到?

参考

这里推荐几篇相关的文档

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值