在Java 8中转换Lambda函数

15

Java 8似乎会生成表示Lambda表达式的类。例如,下面的代码:

  Runnable r = app::doStuff;

表现为清单文件:

  // $FF: synthetic class
  final class App$$Lambda$1 implements Runnable {
    private final App arg$1;

    private App$$Lambda$1(App var1) {
        this.arg$1 = var1;
    }

    private static Runnable get$Lambda(App var0) {
        return new App$$Lambda$1(var0);
    }

    public void run() {
        this.arg$1.doStuff();
    }
  }

据我所理解,这段代码是在运行时生成的。现在,假设某人想要将代码注入上述类的run方法中。目前的实验结果产生了一些NoClassDefFoundVerifyError的混合:

java.lang.NoClassDefFoundError: App$$Lambda$2
    at App$$Lambda$2/1329552164.run(Unknown Source)
    at App.main(App.java:9)
Caused by: java.lang.ClassNotFoundException: App$$Lambda$2
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 2 more

正在运行的内容是:

$ java -version
java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)

在将任何新的字节码推入类之前,就已经出现了这种情况。

这是预期的吗?感觉像是JDK的bug,但我很高兴能够被证明是错误的!

这里有一个Github仓库展示了这种行为


1
验证错误表明您创建了损坏的字节码。您尝试过调试代码吗?错误发生在哪个操作期间? - Rafael Winterhalter
1
你从转换器中返回了一个空数组,这样做可能会出问题。最好返回 null 来表示你不想转换该类。 - Rafael Winterhalter
3
请记住,Lambda表达式和运行时类的映射是有意不确定的。多个Lambda表达式可能共享一个类,或者相同的表达式在运行时可能被不同的、不断变化的类所表示。规范明确说明了这些可能性。因此,即使是Instrumentation API也被修改为允许您对正在处理的这样的类进行插装,您仍然要小心谨慎。在某个特定JVM实现的一个版本中可以正常工作的程序可能在下一个修订版本中失败。 - Holger
2
无论你想要实现什么,最好的方法是通过检测创建invokedynamic指令或目标方法来实现。没有理由去检测lambda表达式或方法引用的短暂匿名类。 - Holger
2
你不能假定这种行为会保持不变直到下一个主要更新。因为它明确没有指定,所以它可能会在接下来的次要修订版中立即发生改变。这样的内部更改并非第一次出现。例如,在7u6中,内部字符串表示发生了根本性的变化,并且在8u20中添加了字符串去重功能... - Holger
显示剩余7条评论
2个回答

12
对我来说,这似乎是JVM的一个错误。系统类加载器尝试通过名称定位转换后的类。然而,lambda表达式通过匿名类加载进行加载,其中包含以下条件:
clazz.getClassLoader()
     .loadClass(clazz.getName().substring(0, clazz.getName().indexOf('/')))

产生 NoClassDefErrorClassNotFoundException。这个类不被认为是真正的类,因此这些匿名类例如不会传递给 ClassFileTransformer 以外的重转换。总的来说,当处理匿名类时,仪器API对我来说感觉有点不稳定。同样,LambdaForm 被传递给 ClassFileTransformer,但除了 classFileBuffer 之外的所有参数都设置为 null,这会破坏转换器类的契约。对于你的例子,问题似乎是你返回了 null;当返回 classFileBuffer 时,问题就消失了,这是一个无操作。然而,这并不是 ClassFileTransformer 建议的方式,在那里返回 null 是推荐的做法: "一个格式良好的类文件缓冲区(变换的结果),或者如果没有进行变换,则为 null。" 对我来说,这似乎是HotSpot中的一个错误。你应该向OpenJDK报告这个问题。
总之,我在我的代码操纵库Byte Buddy中展示了可以对匿名加载的类进行插装的可能性。与普通插装相比,需要做一些不太幸运的调整,但运行时支持它。以下是一个成功作为该库单元测试运行的示例:
Callable<String> lambda = () -> "foo";

Instrumentation instrumentation = ByteBuddyAgent.install();
ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.of(instrumentation)
    .preregistered(lambda.getClass());
ClassFileLocator classFileLocator = ClassFileLocator.AgentBased.of(instrumentation, 
     lambda.getClass());

assertThat(lambda.call(), is("foo"));

new ByteBuddy()
  .redefine(lambda.getClass(), classFileLocator)
  .method(named("call"))
  .intercept(FixedValue.value("bar"))
  .make()
  .load(lambda.getClass().getClassLoader(), classReloadingStrategy);

assertThat(lambda.call(), is("bar"));

谢谢 Rafael。我正在联系Oracle和OpenJDK的人员。一旦有了具体结果,我会更新本文。 - BitPusher

5

Oracle的开发团队接受了错误报告,并将其作为JDK-8145964跟踪。这并不完全是一个解决方案,但似乎是一个真实的运行时问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接