Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to read lambda expression bytecode using ASM

How can I read the bytecode instructions from the body of a lambda expression using ASM?

like image 519
Josh Stone Avatar asked May 26 '14 01:05

Josh Stone


2 Answers

EDIT 01-08-2016: I added another method using the SerializedLambda class which does not required a 3-rd party software (i.e., ByteBuddy), you can read about it in the section titled: "Using SerializedLambda" bellow.

Original Answer: Problem Explanation + Solving it Using ByteBuddy

The accepted answer did not contain concrete information about how to actually read the lambda byte code at run-time via asm (i.e., without javap) - so I thought I would add this information here for future reference and the benefit of others.

assume the following code:

public static void main(String[] args) {
    Supplier<Integer> s = () -> 1;
    byte[] bytecode = getByteCodeOf(s.getClass()); //this is the method that we need to create.
    ClassReader reader = new ClassReader(bytecode);
    print(reader); 
}
  • I am assuming that you already have the print(ClassReader) code - if not see the answers to this question.

In order to read the byte-code via asm you first need to give asm (via the ClassReader) the actual byte-code of the lambda - the problem is that the lambda class is generated at runtime via the LambdaMetaFactory class and therefore the normal method of getting the byte code does NOT work:

byte[] getByteCodeOf(Class<?> c){
    //in the following - c.getResourceAsStream will return null..
    try (InputStream input = c.getResourceAsStream('/' + c.getName().replace('.', '/')+ ".class")){
        byte[] result = new byte[input.available()];
        input.read(result);
        return result;
    }
}

If we look on the name of the class c via c.getName() we will see something like defining.class.package.DefiningClass$$Lambda$x/y where x and y are numbers, now we can understand why the above does not work - there is no such resource on the classpath..

Although the JVM obviously knows the bytecode of the class, sadly, it has no ready-made API that allows you to retrieve it, on the other-hand, the JVM has an instrumentation API (via Agents) which allows you to write a class that can inspect the bytecode of a loading (and re-loading) classes.

We could have written such agent, and somehow communicate to it that we want to receive the bytecode of the lambda class - the agent may then request the JVM to reload that class (without changing it) - which will result in the agent receiving the byte-code of the reloading class and returning it back to us.

Luckily for us there is a library called ByteBuddy that have such agent already created, using this library - the following will work (if you are a maven user include dependencies for both byte-buddy-dep and byte-buddy-agent in your pom, also - see notes bellow about limitations).

private static final Instrumentation instrumentation = ByteBuddyAgent.install();

byte[] getByteCodeOf(Class<?> c) throws IOException {
    ClassFileLocator locator = ClassFileLocator.AgentBased.of(instrumentation, c);
    TypeDescription.ForLoadedType desc = new TypeDescription.ForLoadedType(c);
    ClassFileLocator.Resolution resolution = locator.locate(desc.getName());
    return resolution.resolve();
}

Limitation: - Depending on your jvm installation you may have to install the agent via command line (see ByteBuddyAgent documentation and Instrumentation documentation)

New Answer: Using SerializedLambda

If the lambda you are trying to read implements an interface that extends Serializable - the LambdaMetafactory class actually generates a private methods called writeReplace which provides an instance of the class SerializedLambda. This instance can be used to retrieve the actual static method that was generated using the LambdaMetafactory.

So, for example here are 2 ways to have a "Serializable Lambda":

public class Sample {
    interface SerializableRunnable extends Runnable, Serializable{}

    public static void main(String... args) {
        SerializableRunnable oneWay = () -> System.out.println("I am a serializable lambda");

        Runnable anotherWay = (Serializable & Runnable) () -> System.out.println("I am a serializable lambda too!");
    }
}

In the above examples both oneWay and anotherWay will have a generated writeReplace method which can be retrieved using reflection in the following way:

SerializedLambda getSerializedLambda(Serializable lambda) throws Exception {
    final Method method = lambda.getClass().getDeclaredMethod("writeReplace");
    method.setAccessible(true);
    return (SerializedLambda) method.invoke(lambda);
}

If we look at the javadoc of SerializedLambda we will find the following methods:

public String getImplClass(): Get the name of the class containing the implementation method. Returns: the name of the class containing the implementation method

public String getImplMethodName(): Get the name of the implementation method. Returns: the name of the implementation method

Which means that you can now use ASM to read the class containing the lambda, get to the method that implement the lambda and modify/read it.

You can even get a reflective version of the lambda using this code:

Method getReflectedMethod(Serializable lambda) throws Exception {
    SerializedLambda s = getSerializedLambda(lambda);
    Class containingClass = Class.forName(s.getImplClass());
    String methodName = s.getImplMethodName();
    for (Method m : containingClass.getDeclaredMethods()) {
        if (m.getName().equals(methodName)) return m;
    }

    throw new NoSuchElementException("reflected method could not be found");
}
like image 157
bennyl Avatar answered Oct 14 '22 06:10

bennyl


A lambda compiles to a static method with a synthetic name. So to read the code using ASM, you would reverse engineer the method name ... then read it like any other method.

But if you just want to look at the bytecode for the lambda, it is simpler to use javap.

like image 3
Stephen C Avatar answered Oct 14 '22 08:10

Stephen C