Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Transforming lambdas in Java 8

Java 8 appears to generate classes to represent lambda expressions. For instance, the code:

  Runnable r = app::doStuff;

Manifests, roughly, as:

  // $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();
    }
  }

As I understand this, the code is generated at runtime. Now, suppose one wanted to inject code into the run method of the above class. Experiments thus far yield a mix of NoClassDefFound and VerifyError:

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

This is running against:

$ 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)

This is even before pushing any new bytecode into the class.

Is this expected? Smells like a JDK bug, but I'm happy to be wrong!

Here's a Github repo illustrating the behavior

like image 204
thetwan Avatar asked Dec 08 '15 17:12

thetwan


People also ask

Does Java 8 have lambdas?

Lambda Expressions were added in Java 8. A lambda expression is a short block of code which takes in parameters and returns a value. Lambda expressions are similar to methods, but they do not need a name and they can be implemented right in the body of a method.

How do I change lambda with method reference?

If you are using a lambda expression as an anonymous function but not doing anything with the argument passed, you can replace lambda expression with method reference. In the first two cases, the method reference is equivalent to lambda expression that supplies the parameters of the method e.g. System.

Are lambdas immutable?

Lambda expressions help us achieve pure functions, immutability, and first-class functions principles in Java. Lambda functions are pure because they do not rely on a specific class scope. They are immutable because they reference the passed parameter but do not modify the parameter's value to reach their result.


2 Answers

To me, this seems like a bug in the JVM. The system class loader attempts to locate the transformed class by its name. However, lambda expressions are loaded via anonymous class loading where the following condition:

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

yields a ClassNotFoundException resulting in the NoClassDefError. The class is not considered a real class and such anonyoumous classes are for example not passed to a ClassFileTransformer outside of a retransform.

All in all, the instrumentation API feels a bit buggy to me when dealing with anonymous classes. Similarly, LambdaForms are passed to ClassFileTransformers but with all arguments but the classFileBuffer set to null what breaks the transformer class's contract.

For your example, the problem seems to be that you return null; the problem goes away when returning the classFileBuffer what is a no-op. This is however not what the ClassFileTransformer suggests, where returning null is the recommended way of doing this:

a well-formed class file buffer (the result of the transform), or null if no transform is performed.

To me, this seems like a bug in HotSpot. You should report this issue to the OpenJDK.

All in all, it is perfectly possible to instrument anonymously loaded classes as I demonstrate in my code manipulation library Byte Buddy. It requires some unfortunate tweaks compared to normal instrumentation but the runtime supports it. Here is an example that successfully runs as a unit test within the library:

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"));
like image 84
Rafael Winterhalter Avatar answered Oct 12 '22 02:10

Rafael Winterhalter


Bug submission was accepted by folks at Oracle, and is being tracked as JDK-8145964. This isn't exactly a solution, but appears to be a real runtime issue.

like image 21
thetwan Avatar answered Oct 12 '22 03:10

thetwan