简短的答案是:是使用内部调试参数dump出了Class文件,然后人肉反编译Class文件给出了我的回答中的代码。而其实这些步骤只是我基于已有的知识所采用的简略步骤,大家即便不知道实现细节,也可以靠自己一步步探索出来的。
我是如何做的:
首先我知道Oracle JDK 8 / OpenJDK 8对lambda表达式在运行时的实现方式是动态生成跟匿名内部类相似形式的类,而负责生成代码的类位于java.lang.invoke.InnerClassLambdaMetafactory。可以看到,这个类里有一个调试用的Java property可以设置:
jdk.internal.lambda.dumpProxyClasses
相关代码在:
static {
final String key = "jdk.internal.lambda.dumpProxyClasses";
String path = AccessController.doPrivileged(
new GetPropertyAction(key), null,
new PropertyPermission(key , "read"));
dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
}
所以我们在启动Java的时候,传入这个参数:
-Djdk.internal.lambda.dumpProxyClasses=<path_to_your_dump_directory>
就可以让JDK把lambda表达式对应的运行时生成的类给dump下来了。
接下来我会把这些dump下来的Class文件用javap解读成文本形式,然后人肉将其反编译到Java代码。涉及的字节码其实都非常简单,即便人肉反编译也非常非常快。大家如果还是喜欢用现成的反编译器也大可去用自己喜欢的反编译器。
然而我是如何知道“首先”的那步内容的呢?
大家可以如何着手去调查:
即便没有文档也可以顺藤摸瓜,写个例子慢慢调查。
例如说,随便写个这样的例子:
import java.util.Arrays;
public class MyTest {
public static void main(String[] args) {
Runnable r = () -> System.out.println(Arrays.toString(args));
r.run();
}
}
然后用javac -g MyTest.java编译,再用javap来把Class文件解读为文本形式:
$ javap -verbose -private MyTest
Classfile /private/tmp/MyTest.class
Last modified May 28, 2017; size 1260 bytes
MD5 checksum 2ce5e67938afee50b0dc8841569ea12e
Compiled from "MyTest.java"
public class MyTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#25 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#30 // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
#3 = InterfaceMethodref #31.#32 // java/lang/Runnable.run:()V
#4 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #35.#36 // java/util/Arrays.toString:([Ljava/lang/Object;)Ljava/lang/String;
#6 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #39 // MyTest
#8 = Class #40 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LMyTest;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 r
#21 = Utf8 Ljava/lang/Runnable;
#22 = Utf8 lambda$main$0
#23 = Utf8 SourceFile
#24 = Utf8 MyTest.java
#25 = NameAndType #9:#10 // "<init>":()V
#26 = Utf8 BootstrapMethods
#27 = MethodHandle #6:#41 // 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;
#28 = MethodType #10 // ()V
#29 = MethodHandle #6:#42 // invokestatic MyTest.lambda$main$0:([Ljava/lang/String;)V
#30 = NameAndType #43:#44 // run:([Ljava/lang/String;)Ljava/lang/Runnable;
#31 = Class #45 // java/lang/Runnable
#32 = NameAndType #43:#10 // run:()V
#33 = Class #46 // java/lang/System
#34 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#35 = Class #49 // java/util/Arrays
#36 = NameAndType #50:#51 // toString:([Ljava/lang/Object;)Ljava/lang/String;
#37 = Class #52 // java/io/PrintStream
#38 = NameAndType #53:#54 // println:(Ljava/lang/String;)V
#39 = Utf8 MyTest
#40 = Utf8 java/lang/Object
#41 = Methodref #55.#56 // 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;
#42 = Methodref #7.#57 // MyTest.lambda$main$0:([Ljava/lang/String;)V
#43 = Utf8 run
#44 = Utf8 ([Ljava/lang/String;)Ljava/lang/Runnable;
#45 = Utf8 java/lang/Runnable
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 java/util/Arrays
#50 = Utf8 toString
#51 = Utf8 ([Ljava/lang/Object;)Ljava/lang/String;
#52 = Utf8 java/io/PrintStream
#53 = Utf8 println
#54 = Utf8 (Ljava/lang/String;)V
#55 = Class #58 // java/lang/invoke/LambdaMetafactory
#56 = NameAndType #59:#63 // 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;
#57 = NameAndType #22:#17 // lambda$main$0:([Ljava/lang/String;)V
#58 = Utf8 java/lang/invoke/LambdaMetafactory
#59 = Utf8 metafactory
#60 = Class #65 // java/lang/invoke/MethodHandles$Lookup
#61 = Utf8 Lookup
#62 = Utf8 InnerClasses
#63 = Utf8 (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;
#64 = Class #66 // java/lang/invoke/MethodHandles
#65 = Utf8 java/lang/invoke/MethodHandles$Lookup
#66 = Utf8 java/lang/invoke/MethodHandles
{
public MyTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LMyTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: invokedynamic #2, 0 // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
6: astore_1
7: aload_1
8: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
13: return
LineNumberTable:
line 5: 0
line 6: 7
line 7: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 args [Ljava/lang/String;
7 7 1 r Ljava/lang/Runnable;
private static void lambda$main$0(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokestatic #5 // Method java/util/Arrays.toString:([Ljava/lang/Object;)Ljava/lang/String;
7: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 args [Ljava/lang/String;
}
SourceFile: "MyTest.java"
InnerClasses:
public static final #61= #60 of #64; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #27 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:
#28 ()V
#29 invokestatic MyTest.lambda$main$0:([Ljava/lang/String;)V
#28 ()V
(Whenever I open this page in Chrome with the macOS Chinese input method active, Chrome crashes immediately. I'll have to write the following in English for now. Hopefully the crash issue goes away soon so that I can rewrite it back to Chinese.)
As shown above, the two lines of source code in main() maps to the following bytecode by javac:
// 1st line:
// Runnable r = () -> System.out.println(Arrays.toString(args));
0: aload_0
1: invokedynamic #2, 0 // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
6: astore_1
// 2nd line:
// r.run();
7: aload_1
8: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
The 2nd line, which invokes the lambda, is the same as a normal Runnable.run() invocation, through invokeinterface. So it doesn't matter whether the Runnable was created using a lambda expression or some other way (e.g. pre-Java 8 anonymous inner classes), the invocation side is always the same.
The interesting part is on the 1st line, where there's a curious-looking invokedynamic instruction involved. Looking at its operands, "#2", is:
#2 = InvokeDynamic #0:#30 // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
which specifies a "BootstrapMethod" (#0) and a "NameAndType" (#30). Let's look at the BootstrapMethod part:
BootstrapMethods:
0: #27 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:
#28 ()V
#29 invokestatic MyTest.lambda$main$0:([Ljava/lang/String;)V
#28 ()V
(Note that the body of the lambda expression is actually desugared into a private static method, lambda$main$0.)
Here we can see that the bootstrap method specified is 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;, from which we can see its use of the InnerClassLambdaMetaFactory:
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
That's where we would start following the call and dig in, and then search for keywords like "dump", "trace", "debug", etc, to see if there's any debugging switch/flags embedded in the implementation that would give us more information for debugging.
Have fun debugging!
What Else?
Of course, an even better way to get started is to RTFM. Here's the design documentation of the lambda expression translation strategy: Translation of Lambda Expressions