Lambda 表达式和匿名 类 在加载为隐藏 类 时不起作用
Lambda expressions and anonymous classes don't work when loaded as hidden classes
我正在尝试在运行时编译和加载动态生成的 Java 代码。由于 ClassLoader::defineClass 和 Unsafe::defineAnonymousClass 在这种情况下都有严重的缺点,我尝试使用 hidden classes via Lookup::defineHiddenClass 代替。这适用于我尝试加载的所有 classes,除了调用 lambda 表达式或包含匿名 classes 的那些 classes。
调用 lambda 表达式会引发以下异常:
Exception in thread "main" java.lang.NoClassDefFoundError: tests/HiddenClassLambdaTest$LambdaRunner/0x0000000800c04400
at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:22)
Caused by: java.lang.ClassNotFoundException: tests.HiddenClassLambdaTest$LambdaRunner.0x0000000800c04400
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:636)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:182)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:519)
... 1 more
执行实例化匿名的代码 class 会引发以下错误:
Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
Location:
tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400.run()V @5: invokespecial
Reason:
Type 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' (current frame, stack[2]) is not assignable to 'tests/HiddenClassLambdaTest$LambdaRunner'
Current Frame:
bci: @5
flags: { }
locals: { 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
stack: { uninitialized 0, uninitialized 0, 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
Bytecode:
0000000: bb00 1159 2ab7 0013 4cb1
at java.base/java.lang.ClassLoader.defineClass0(Native Method)
at java.base/java.lang.System.defineClass(System.java:2193)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2446)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClassAsLookup(MethodHandles.java:2427)
at java.base/java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2133)
at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:25)
这是一个重现问题的简短示例:
import java.lang.invoke.MethodHandles;
public class HiddenClassLambdaTest {
/** This class is to be loaded and executed as hidden class */
public static final class LambdaRunner implements Runnable {
@Override public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
public static void main(String[] args) throws Throwable {
// Path to the class file of the nested class defined above
String nestedClassPath = HiddenClassLambdaTest.class.getTypeName().replace('.','/') + "$LambdaRunner.class";
// Class file content of the LambdaRunner class
byte[] classFileContents = HiddenClassLambdaTest.class.getClassLoader().getResourceAsStream(nestedClassPath).readAllBytes();
Class<?> lambdaRunnerClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true).lookupClass();
Runnable lambdaRunnerInstance = (Runnable) lambdaRunnerClass.getConstructor().newInstance();
lambdaRunnerInstance.run();
}
}
我已经尝试用不同的 JDK 编译和 运行 代码,使用不同的方式创建隐藏 class 的新实例,在 https://bugs.openjdk.java.net/ 搜索错误,搞砸了与字节码本身和其他一些东西。我不是 Java 内部专家,所以我不确定我是否没有正确理解引入隐藏 classes 的 JEP。
我是不是做错了什么,这是不可能的还是一个错误?
编辑:JEP 状态
Migration should take the following into account:
To invoke private nestmate instance methods from code in a hidden class, use invokevirtual or invokeinterface instead of invokespecial. Generated bytecode that uses invokespecial to invoke a private nestmate instance method will fail verification. invokespecial should only be used to invoke private nestmate constructors.
这可能是匿名 class 的问题。有没有一种编译代码的方法可以避免在字节码中使用 invokespecial?
你不能把任意 classes 变成隐藏的 classes.
documentation of defineHiddenClass
包含句子
- On any attempt to resolve the entry in the run-time constant pool indicated by
this_class
, the symbolic reference is considered to be resolved to C
and resolution always succeeds immediately.
它没有明确说明的是,这是唯一一个类型解析最终出现在隐藏的地方 class。
但在bug report JDK-8222730中已经明确表示:
For a hidden class, its specified hidden name should only be accessible through the hidden class's 'this_class' constant pool entry.
The class should not be accessible by specifying its original name in, for example, a method or field signature even within the hidden class.
我们可以检查一下。即使是像
这样简单的案例
public class HiddenClassLambdaTest {
public static void main(String[] args) throws Throwable {
byte[] classFileContents = HiddenClassLambdaTest.class
.getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class")
.readAllBytes();
var hidden = MethodHandles.lookup()
.defineHiddenClass(classFileContents, true, ClassOption.NESTMATE);
Runnable lambdaRunnerInstance = (Runnable)hidden.findConstructor(
hidden.lookupClass(), MethodType.methodType(void.class)).invoke();
lambdaRunnerInstance.run();
}
static class LambdaRunner implements Runnable {
LambdaRunner field = this;
@Override
public void run() {
}
}
}
已经失败了。请注意,在隐藏的 class 中解析原始 class 名称 LambdaRunner
的尝试不会失败,这是一种特殊情况,因为您使用了现有的 class 作为模板。因此,由于隐藏的 class 和现有的 LambdaRunner
class 之间的不匹配,您会得到 IncompatibleClassChangeError
或 VerifierError
。当您不使用现有 class 的 class 定义时,您会得到 NoClassDefFoundError
.
同样适用于
static class LambdaRunner implements Runnable {
static void method(LambdaRunner arg) {
}
@Override
public void run() {
method(this);
}
}
正如引用的错误报告所说,字段和方法都不能引用其签名中隐藏的 class。
一个不太直观的例子是
static class LambdaRunner implements Runnable {
@Override
public void run() {
System.out.println("" + this);
}
}
根据编译器和选项的不同,这将失败,因为当使用 StringConcatFactory
时,行为就像调用一个将所有 non-constant 部分作为参数并返回 String
。所以这是在方法签名中隐藏 class 的另一种情况。
Lambda 表达式很特殊,class 就像
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
编译类似于
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = LambdaRunner::lambdaBody;
runnable.run();
}
private static void lambdaBody() {
System.out.println("Success");
}
}
它在方法签名中没有隐藏的 class,但必须引用包含 lambda 表达式主体的方法作为 MethodReference
。在常量池中,该方法的描述是指它使用 this_class
条目声明 class。因此它被重定向到隐藏的 class,如文档中所述。
但是作为 MethodReference
一部分的 MethodType
的构造不会像 class 文字那样使用此信息来加载 Class
。相反,它会尝试通过定义的 class 加载程序加载隐藏的 class,该加载程序因您发布的 NoClassDefFoundError
而失败。
这似乎与 JDK-8130087 有关,这表明普通方法解析与 MethodType
工作方式不同,这可能会使 MethodType
在仅调用该方法的情况下失败.
但有可能证明即使解决这个问题也不能解决一般问题:
static class LambdaRunner implements Runnable {
@Override
public void run() {
var lookup = MethodHandles.lookup();
var noArgVoid = MethodType.methodType(void.class);
try {
MethodHandle mh = LambdaMetafactory.metafactory(lookup, "run",
MethodType.methodType(Runnable.class), noArgVoid,
lookup.findStatic(LambdaRunner.class, "lambdaBody", noArgVoid),
noArgVoid).getTarget();
System.out.println("got factory");
Runnable runnable = (Runnable)mh.invokeExact();
System.out.println("got runnable");
runnable.run();
}
catch(RuntimeException|Error e) {
throw e;
}
catch(Throwable e) {
throw new AssertionError(e);
}
}
private static void lambdaBody() {
System.out.println("Success");
}
}
这会绕过上述问题并手动调用 LambdaMetafactory
。当被重新定义为隐藏时class,它会打印:
got factory
got runnable
Exception in thread "main" java.lang.NoClassDefFoundError: test/HiddenClassLambdaTest$LambdaRunner/0x0000000800c01400
at test/test.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:15)
Caused by: java.lang.ClassNotFoundException: test.HiddenClassLambdaTest$LambdaRunner.0x0000000800c01400
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 1 more
这说明绕过了所有的障碍,但是当真正从生成的Runnable
调用到持有lambda体的方法时,会失败,因为目标class 是 隐藏的 。急切解析符号引用的 JVM 可能会更早失败,即示例可能不会打印 got runnable
然后。
不像旧的JVM匿名classes,没有办法link到一个隐藏的class,甚至不能从另一个隐藏的class.
最重要的是,正如开头所说,您不能将任意 classes 变成隐藏的 classes。 Lambda 表达式并不是唯一不能使用隐藏 classes 的特性。尝试并感到惊讶不是一个好主意。隐藏的 classes 只能与字节码生成器结合使用,谨慎使用已知可用的功能。
正如 Holger 指出的那样,您不能使用 MethodHandles.Lookup.defineHiddenClass
.
加载任意 classes
但是您可以对 class 文件进行一些转换以符合隐藏 class.
的限制
一个这样的转换是使用 MethodHandles::invokeExact
作为实现方法 - 因此 lambda 将捕获 MethodHandle
.
以您的代码为基础,我想到了这个 - gist with imports and w/o comments:
public class HiddenClassLambdaTest {
/** This class is to be loaded and executed as hidden class */
public static final class LambdaRunner implements Runnable {
@Override public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
public static void main(String[] args) throws Throwable {
// Path to the class file of the nested class defined above
byte[] classFileContents = HiddenClassLambdaTest.class
.getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class").readAllBytes();
classFileContents = processLambdas(classFileContents);
MethodHandles.Lookup hiddenClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true);
Runnable lambdaRunnerInstance = (Runnable) (
hiddenClass.findConstructor(hiddenClass.lookupClass(), methodType(void.class))
).asType(methodType(Runnable.class)).invokeExact();
lambdaRunnerInstance.run();
}
我们开始喜欢您的示例代码 - 除了我们在将 classFileContents
传递给 MethodHandles.Lookup.defineHiddenClass
之前使用 ASM 对其进行处理。
public static CallSite metafactory(MethodHandles.Lookup l, String name, MethodType mt,
MethodType interfaceType, MethodHandle mh, MethodType dynamicMethodType) throws Throwable {
MethodHandle invoker = MethodHandles.exactInvoker(mh.type());
if (mt.parameterCount() == 0) {
// Non-capturing lambda
mt = mt.appendParameterTypes(MethodHandle.class);
CallSite cs = LambdaMetafactory.metafactory(l, name, mt, interfaceType, invoker, dynamicMethodType);
Object instance = cs.dynamicInvoker().asType(methodType(Object.class, MethodHandle.class)).invokeExact(mh);
return new ConstantCallSite(MethodHandles.constant(mt.returnType(), instance));
} else {
// capturing
MethodType lambdaMt = mt.insertParameterTypes(0, MethodHandle.class);
CallSite cs = LambdaMetafactory.metafactory(l, name, lambdaMt, interfaceType, invoker, dynamicMethodType);
return new ConstantCallSite(cs.dynamicInvoker().bindTo(mh));
}
}
作为技巧的一部分,我们将所有指向 LambdaMetafactory.metafactory
的 invokedynamic 指令替换为 HiddenClassLambdaTest.metafactory
。
在那里我们用 MethodHandles.invokeExact
替换实现方法,然后添加原始 MethodHandle 作为捕获参数的一部分。
您可能希望将该方法移动到不同的 class。
public static CallSite altMetafactory(MethodHandles.Lookup l, String name, MethodType mt, Object... args) {
throw new UnsupportedOperationException("Not Implemented");
}
与 LambdaMetafactory.altMetafactory
相同。除了我没有实施它。 ¯\(ツ)/¯
private static byte[] processLambdas(byte[] bytes) {
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = cw;
cv = new LambdaTransformer(cv);
cr.accept(cv, 0);
return cw.toByteArray();
}
只是 ASM 转换的常用样板文件。
因为我预计我必须,所以我就这样写了。
private static class LambdaTransformer extends ClassVisitor {
LambdaTransformer(ClassVisitor parent) {
super(ASM9, parent);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
return new LambdaMethodTransformer(super.visitMethod(access, name, descriptor, signature, exceptions));
}
private static class LambdaMethodTransformer extends MethodVisitor {
public LambdaMethodTransformer(MethodVisitor parent) {
super(ASM9, parent);
}
private static final ClassDesc CD_LambdaMetafactory = LambdaMetafactory.class.describeConstable().orElseThrow();
private static final ClassDesc CD_HiddenLambdaTest = HiddenClassLambdaTest.class.describeConstable().orElseThrow();
private static final DirectMethodHandleDesc LMF_FACTORY = ofCallsiteBootstrap(
CD_LambdaMetafactory, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType);
private static final DirectMethodHandleDesc LMF_ALTFACTORY = ofCallsiteBootstrap(
CD_LambdaMetafactory, "altMetafactory", CD_Object.arrayType());
private static final Handle MY_FACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType));
private static final Handle MY_ALTFACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "altMetafactory", CD_CallSite, CD_Object.arrayType()));
@Override
public void visitInvokeDynamicInsn(String name, String descriptor,
Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
MethodHandleDesc h = fromASM(bootstrapMethodHandle);
if (h.equals(LMF_FACTORY)) {
super.visitInvokeDynamicInsn(name, descriptor,
MY_FACTORY, bootstrapMethodArguments);
} else if (h.equals(LMF_ALTFACTORY)) {
super.visitInvokeDynamicInsn(name, descriptor, MY_ALTFACTORY, bootstrapMethodArguments);
} else {
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
}
这是重要的部分 - 检查 invokedynamic 指令的 bootstrap 方法是否为 LambdaMetafactory.metafactory
或 LambdaMetafactory.altMetafactory
并将其替换为 HiddenClassLambdaTest.metafactory
或 HiddenClassLambdaTest.altMetafactory
分别。
private static MethodHandleDesc fromASM(Handle h) {
return MethodHandleDesc.of(Kind.valueOf(h.getTag(), h.isInterface()),
ClassDesc.ofDescriptor("L" + h.getOwner() + ";"),
h.getName(), h.getDesc());
}
private static Handle toASM(DirectMethodHandleDesc desc) {
return new Handle(desc.refKind(), toInternal(desc.owner()), desc.methodName(), desc.lookupDescriptor(), desc.isOwnerInterface());
}
private static String toInternal(TypeDescriptor.OfField<?> desc) {
String d = desc.descriptorString();
if (d.charAt(0) != 'L') {
throw new IllegalArgumentException("Not a valid internal type: " + d);
}
return d.substring(1, d.length() - 1); // Strip "L" + ";"
}
}
}
}
最后一些辅助方法让我可以轻松地在 ASM 类型和 java.lang.constant.*
类型之间进行转换。
我更喜欢使用 java.lang.constant.*
API - 这些辅助方法通常是从实用程序 class.
静态导入的
但这只是故事的一半。
有关更多可能出错的事情,请参阅 Holger 的回答。
其中一些有一个“简单”的解决方案,其他的可能更复杂。
例如,要删除引用 class 的字段,请将字段类型替换为超级 class(例如 java.lang.Object
)并注入 checkcast
在这些字段的每个 getfield
或 getstatic
之后的指令。
要将它们从方法签名中移除 - 您还必须更改调用站点的签名。
我正在尝试在运行时编译和加载动态生成的 Java 代码。由于 ClassLoader::defineClass 和 Unsafe::defineAnonymousClass 在这种情况下都有严重的缺点,我尝试使用 hidden classes via Lookup::defineHiddenClass 代替。这适用于我尝试加载的所有 classes,除了调用 lambda 表达式或包含匿名 classes 的那些 classes。
调用 lambda 表达式会引发以下异常:
Exception in thread "main" java.lang.NoClassDefFoundError: tests/HiddenClassLambdaTest$LambdaRunner/0x0000000800c04400
at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:22)
Caused by: java.lang.ClassNotFoundException: tests.HiddenClassLambdaTest$LambdaRunner.0x0000000800c04400
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:636)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:182)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:519)
... 1 more
执行实例化匿名的代码 class 会引发以下错误:
Exception in thread "main" java.lang.VerifyError: Bad type on operand stack
Exception Details:
Location:
tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400.run()V @5: invokespecial
Reason:
Type 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' (current frame, stack[2]) is not assignable to 'tests/HiddenClassLambdaTest$LambdaRunner'
Current Frame:
bci: @5
flags: { }
locals: { 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
stack: { uninitialized 0, uninitialized 0, 'tests/HiddenClassLambdaTest$LambdaRunner+0x0000000800c00400' }
Bytecode:
0000000: bb00 1159 2ab7 0013 4cb1
at java.base/java.lang.ClassLoader.defineClass0(Native Method)
at java.base/java.lang.System.defineClass(System.java:2193)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClass(MethodHandles.java:2446)
at java.base/java.lang.invoke.MethodHandles$Lookup$ClassDefiner.defineClassAsLookup(MethodHandles.java:2427)
at java.base/java.lang.invoke.MethodHandles$Lookup.defineHiddenClass(MethodHandles.java:2133)
at tests.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:25)
这是一个重现问题的简短示例:
import java.lang.invoke.MethodHandles;
public class HiddenClassLambdaTest {
/** This class is to be loaded and executed as hidden class */
public static final class LambdaRunner implements Runnable {
@Override public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
public static void main(String[] args) throws Throwable {
// Path to the class file of the nested class defined above
String nestedClassPath = HiddenClassLambdaTest.class.getTypeName().replace('.','/') + "$LambdaRunner.class";
// Class file content of the LambdaRunner class
byte[] classFileContents = HiddenClassLambdaTest.class.getClassLoader().getResourceAsStream(nestedClassPath).readAllBytes();
Class<?> lambdaRunnerClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true).lookupClass();
Runnable lambdaRunnerInstance = (Runnable) lambdaRunnerClass.getConstructor().newInstance();
lambdaRunnerInstance.run();
}
}
我已经尝试用不同的 JDK 编译和 运行 代码,使用不同的方式创建隐藏 class 的新实例,在 https://bugs.openjdk.java.net/ 搜索错误,搞砸了与字节码本身和其他一些东西。我不是 Java 内部专家,所以我不确定我是否没有正确理解引入隐藏 classes 的 JEP。
我是不是做错了什么,这是不可能的还是一个错误?
编辑:JEP 状态
Migration should take the following into account: To invoke private nestmate instance methods from code in a hidden class, use invokevirtual or invokeinterface instead of invokespecial. Generated bytecode that uses invokespecial to invoke a private nestmate instance method will fail verification. invokespecial should only be used to invoke private nestmate constructors.
这可能是匿名 class 的问题。有没有一种编译代码的方法可以避免在字节码中使用 invokespecial?
你不能把任意 classes 变成隐藏的 classes.
documentation of defineHiddenClass
包含句子
- On any attempt to resolve the entry in the run-time constant pool indicated by
this_class
, the symbolic reference is considered to be resolved toC
and resolution always succeeds immediately.
它没有明确说明的是,这是唯一一个类型解析最终出现在隐藏的地方 class。
但在bug report JDK-8222730中已经明确表示:
For a hidden class, its specified hidden name should only be accessible through the hidden class's 'this_class' constant pool entry.
The class should not be accessible by specifying its original name in, for example, a method or field signature even within the hidden class.
我们可以检查一下。即使是像
这样简单的案例public class HiddenClassLambdaTest {
public static void main(String[] args) throws Throwable {
byte[] classFileContents = HiddenClassLambdaTest.class
.getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class")
.readAllBytes();
var hidden = MethodHandles.lookup()
.defineHiddenClass(classFileContents, true, ClassOption.NESTMATE);
Runnable lambdaRunnerInstance = (Runnable)hidden.findConstructor(
hidden.lookupClass(), MethodType.methodType(void.class)).invoke();
lambdaRunnerInstance.run();
}
static class LambdaRunner implements Runnable {
LambdaRunner field = this;
@Override
public void run() {
}
}
}
已经失败了。请注意,在隐藏的 class 中解析原始 class 名称 LambdaRunner
的尝试不会失败,这是一种特殊情况,因为您使用了现有的 class 作为模板。因此,由于隐藏的 class 和现有的 LambdaRunner
class 之间的不匹配,您会得到 IncompatibleClassChangeError
或 VerifierError
。当您不使用现有 class 的 class 定义时,您会得到 NoClassDefFoundError
.
同样适用于
static class LambdaRunner implements Runnable {
static void method(LambdaRunner arg) {
}
@Override
public void run() {
method(this);
}
}
正如引用的错误报告所说,字段和方法都不能引用其签名中隐藏的 class。
一个不太直观的例子是
static class LambdaRunner implements Runnable {
@Override
public void run() {
System.out.println("" + this);
}
}
根据编译器和选项的不同,这将失败,因为当使用 StringConcatFactory
时,行为就像调用一个将所有 non-constant 部分作为参数并返回 String
。所以这是在方法签名中隐藏 class 的另一种情况。
Lambda 表达式很特殊,class 就像
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
编译类似于
static class LambdaRunner implements Runnable {
@Override
public void run() {
Runnable runnable = LambdaRunner::lambdaBody;
runnable.run();
}
private static void lambdaBody() {
System.out.println("Success");
}
}
它在方法签名中没有隐藏的 class,但必须引用包含 lambda 表达式主体的方法作为 MethodReference
。在常量池中,该方法的描述是指它使用 this_class
条目声明 class。因此它被重定向到隐藏的 class,如文档中所述。
但是作为 MethodReference
一部分的 MethodType
的构造不会像 class 文字那样使用此信息来加载 Class
。相反,它会尝试通过定义的 class 加载程序加载隐藏的 class,该加载程序因您发布的 NoClassDefFoundError
而失败。
这似乎与 JDK-8130087 有关,这表明普通方法解析与 MethodType
工作方式不同,这可能会使 MethodType
在仅调用该方法的情况下失败.
但有可能证明即使解决这个问题也不能解决一般问题:
static class LambdaRunner implements Runnable {
@Override
public void run() {
var lookup = MethodHandles.lookup();
var noArgVoid = MethodType.methodType(void.class);
try {
MethodHandle mh = LambdaMetafactory.metafactory(lookup, "run",
MethodType.methodType(Runnable.class), noArgVoid,
lookup.findStatic(LambdaRunner.class, "lambdaBody", noArgVoid),
noArgVoid).getTarget();
System.out.println("got factory");
Runnable runnable = (Runnable)mh.invokeExact();
System.out.println("got runnable");
runnable.run();
}
catch(RuntimeException|Error e) {
throw e;
}
catch(Throwable e) {
throw new AssertionError(e);
}
}
private static void lambdaBody() {
System.out.println("Success");
}
}
这会绕过上述问题并手动调用 LambdaMetafactory
。当被重新定义为隐藏时class,它会打印:
got factory
got runnable
Exception in thread "main" java.lang.NoClassDefFoundError: test/HiddenClassLambdaTest$LambdaRunner/0x0000000800c01400
at test/test.HiddenClassLambdaTest.main(HiddenClassLambdaTest.java:15)
Caused by: java.lang.ClassNotFoundException: test.HiddenClassLambdaTest$LambdaRunner.0x0000000800c01400
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
... 1 more
这说明绕过了所有的障碍,但是当真正从生成的Runnable
调用到持有lambda体的方法时,会失败,因为目标class 是 隐藏的 。急切解析符号引用的 JVM 可能会更早失败,即示例可能不会打印 got runnable
然后。
不像旧的JVM匿名classes,没有办法link到一个隐藏的class,甚至不能从另一个隐藏的class.
最重要的是,正如开头所说,您不能将任意 classes 变成隐藏的 classes。 Lambda 表达式并不是唯一不能使用隐藏 classes 的特性。尝试并感到惊讶不是一个好主意。隐藏的 classes 只能与字节码生成器结合使用,谨慎使用已知可用的功能。
正如 Holger 指出的那样,您不能使用 MethodHandles.Lookup.defineHiddenClass
.
但是您可以对 class 文件进行一些转换以符合隐藏 class.
的限制一个这样的转换是使用 MethodHandles::invokeExact
作为实现方法 - 因此 lambda 将捕获 MethodHandle
.
以您的代码为基础,我想到了这个 - gist with imports and w/o comments:
public class HiddenClassLambdaTest {
/** This class is to be loaded and executed as hidden class */
public static final class LambdaRunner implements Runnable {
@Override public void run() {
Runnable runnable = () -> System.out.println("Success");
runnable.run();
}
}
public static void main(String[] args) throws Throwable {
// Path to the class file of the nested class defined above
byte[] classFileContents = HiddenClassLambdaTest.class
.getResourceAsStream("HiddenClassLambdaTest$LambdaRunner.class").readAllBytes();
classFileContents = processLambdas(classFileContents);
MethodHandles.Lookup hiddenClass = MethodHandles.lookup().defineHiddenClass(classFileContents, true);
Runnable lambdaRunnerInstance = (Runnable) (
hiddenClass.findConstructor(hiddenClass.lookupClass(), methodType(void.class))
).asType(methodType(Runnable.class)).invokeExact();
lambdaRunnerInstance.run();
}
我们开始喜欢您的示例代码 - 除了我们在将 classFileContents
传递给 MethodHandles.Lookup.defineHiddenClass
之前使用 ASM 对其进行处理。
public static CallSite metafactory(MethodHandles.Lookup l, String name, MethodType mt,
MethodType interfaceType, MethodHandle mh, MethodType dynamicMethodType) throws Throwable {
MethodHandle invoker = MethodHandles.exactInvoker(mh.type());
if (mt.parameterCount() == 0) {
// Non-capturing lambda
mt = mt.appendParameterTypes(MethodHandle.class);
CallSite cs = LambdaMetafactory.metafactory(l, name, mt, interfaceType, invoker, dynamicMethodType);
Object instance = cs.dynamicInvoker().asType(methodType(Object.class, MethodHandle.class)).invokeExact(mh);
return new ConstantCallSite(MethodHandles.constant(mt.returnType(), instance));
} else {
// capturing
MethodType lambdaMt = mt.insertParameterTypes(0, MethodHandle.class);
CallSite cs = LambdaMetafactory.metafactory(l, name, lambdaMt, interfaceType, invoker, dynamicMethodType);
return new ConstantCallSite(cs.dynamicInvoker().bindTo(mh));
}
}
作为技巧的一部分,我们将所有指向 LambdaMetafactory.metafactory
的 invokedynamic 指令替换为 HiddenClassLambdaTest.metafactory
。
在那里我们用 MethodHandles.invokeExact
替换实现方法,然后添加原始 MethodHandle 作为捕获参数的一部分。
您可能希望将该方法移动到不同的 class。
public static CallSite altMetafactory(MethodHandles.Lookup l, String name, MethodType mt, Object... args) {
throw new UnsupportedOperationException("Not Implemented");
}
与 LambdaMetafactory.altMetafactory
相同。除了我没有实施它。 ¯\(ツ)/¯
private static byte[] processLambdas(byte[] bytes) {
ClassReader cr = new ClassReader(bytes);
ClassWriter cw = new ClassWriter(cr, 0);
ClassVisitor cv = cw;
cv = new LambdaTransformer(cv);
cr.accept(cv, 0);
return cw.toByteArray();
}
只是 ASM 转换的常用样板文件。
因为我预计我必须
private static class LambdaTransformer extends ClassVisitor {
LambdaTransformer(ClassVisitor parent) {
super(ASM9, parent);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
return new LambdaMethodTransformer(super.visitMethod(access, name, descriptor, signature, exceptions));
}
private static class LambdaMethodTransformer extends MethodVisitor {
public LambdaMethodTransformer(MethodVisitor parent) {
super(ASM9, parent);
}
private static final ClassDesc CD_LambdaMetafactory = LambdaMetafactory.class.describeConstable().orElseThrow();
private static final ClassDesc CD_HiddenLambdaTest = HiddenClassLambdaTest.class.describeConstable().orElseThrow();
private static final DirectMethodHandleDesc LMF_FACTORY = ofCallsiteBootstrap(
CD_LambdaMetafactory, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType);
private static final DirectMethodHandleDesc LMF_ALTFACTORY = ofCallsiteBootstrap(
CD_LambdaMetafactory, "altMetafactory", CD_Object.arrayType());
private static final Handle MY_FACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "metafactory", CD_CallSite, CD_MethodType, CD_MethodHandle, CD_MethodType));
private static final Handle MY_ALTFACTORY = toASM(ofCallsiteBootstrap(CD_HiddenLambdaTest, "altMetafactory", CD_CallSite, CD_Object.arrayType()));
@Override
public void visitInvokeDynamicInsn(String name, String descriptor,
Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
MethodHandleDesc h = fromASM(bootstrapMethodHandle);
if (h.equals(LMF_FACTORY)) {
super.visitInvokeDynamicInsn(name, descriptor,
MY_FACTORY, bootstrapMethodArguments);
} else if (h.equals(LMF_ALTFACTORY)) {
super.visitInvokeDynamicInsn(name, descriptor, MY_ALTFACTORY, bootstrapMethodArguments);
} else {
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
}
}
这是重要的部分 - 检查 invokedynamic 指令的 bootstrap 方法是否为 LambdaMetafactory.metafactory
或 LambdaMetafactory.altMetafactory
并将其替换为 HiddenClassLambdaTest.metafactory
或 HiddenClassLambdaTest.altMetafactory
分别。
private static MethodHandleDesc fromASM(Handle h) {
return MethodHandleDesc.of(Kind.valueOf(h.getTag(), h.isInterface()),
ClassDesc.ofDescriptor("L" + h.getOwner() + ";"),
h.getName(), h.getDesc());
}
private static Handle toASM(DirectMethodHandleDesc desc) {
return new Handle(desc.refKind(), toInternal(desc.owner()), desc.methodName(), desc.lookupDescriptor(), desc.isOwnerInterface());
}
private static String toInternal(TypeDescriptor.OfField<?> desc) {
String d = desc.descriptorString();
if (d.charAt(0) != 'L') {
throw new IllegalArgumentException("Not a valid internal type: " + d);
}
return d.substring(1, d.length() - 1); // Strip "L" + ";"
}
}
}
}
最后一些辅助方法让我可以轻松地在 ASM 类型和 java.lang.constant.*
类型之间进行转换。
我更喜欢使用 java.lang.constant.*
API - 这些辅助方法通常是从实用程序 class.
但这只是故事的一半。
有关更多可能出错的事情,请参阅 Holger 的回答。
其中一些有一个“简单”的解决方案,其他的可能更复杂。
例如,要删除引用 class 的字段,请将字段类型替换为超级 class(例如 java.lang.Object
)并注入 checkcast
在这些字段的每个 getfield
或 getstatic
之后的指令。
要将它们从方法签名中移除 - 您还必须更改调用站点的签名。