【问题标题】:Transforming lambdas in Java 8在 Java 8 中转换 lambda
【发布时间】:2016-01-05 13:34:30
【问题描述】:

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 错误,但我很高兴错了!

这是Github repo illustrating the behavior

【问题讨论】:

  • 验证错误表明您创建了损坏的字节码。您是否尝试调试代码?错误发生在什么操作期间?
  • 我测试了一个 lambda 重新转换,它可以正常工作。你生成的代码一定有问题!
  • 请记住,lambda 表达式和运行时类的映射是有意未指定的。多个 lambda 表达式可能共享一个类,或者同一表达式可能在运行时由不同的、不断变化的类表示。规范明确说明了这些可能性。因此,即使是 Instrumentation API 也得到了修复,以允许您对这样一个您如履薄冰的类进行检测。与特定 JVM 实现的一个特定版本一起工作的情况可能会在下一个版本中失败。
  • 无论您想要实现什么,您最好通过检测创建invokedynamic 指令或目标方法来实现。不应有任何理由检测 lambda 表达式或方法引用的临时匿名类。
  • 您不能假设这种行为在下一次重大更新之前保持不变。由于它是明确未指定的,它可能会在下一个小修订版中更改。这种内部结构的变化并不是第一次。例如,7u6 中的内部字符串表示发生了根本性的变化,8u20 中添加了字符串重复数据删除功能……

标签: java java-8 bytecode instrumentation


【解决方案1】:

对我来说,这似乎是 JVM 中的一个错误。系统类加载器尝试通过名称定位转换后的类。但是,lambda 表达式是通过匿名类加载来加载的,其中满足以下条件:

clazz.getClassLoader()
     .loadClass(clazz.getName().substring(0, clazz.getName().indexOf('/')))

产生ClassNotFoundException 导致NoClassDefError。该类不被视为真正的类,例如,此类匿名类不会在重新转换之外传递给ClassFileTransformer

总而言之,在处理匿名类时,检测 API 对我来说有点错误。同样,LambdaForms 传递给 ClassFileTransformers,但除了 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"));

【讨论】:

  • 谢谢拉斐尔。我正在联系 Oracle 和 OpenJDK 人员。一旦我得到具体的东西,就会更新帖子。
【解决方案2】:

错误提交已被 Oracle 人员接受,并被跟踪为 JDK-8145964。这并不完全是一个解决方案,但似乎是一个真正的运行时问题。

【讨论】:

    猜你喜欢
    • 2016-09-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-26
    • 1970-01-01
    相关资源
    最近更新 更多