Search code examples
javajavassist

When using javassist anonymous inner class how to access instance variables of outer class?


Below is the anonymous inner class definition:

package com.demo;

public class OuterClass {

    private static int staticNum = 1;

    private int instanceNum  = 2;

    public Runnable redefineMe() {
        return new Runnable() {
            @Override
            public void run() {
                System.out.printf("staticNum %d, instanceNum %d \n", staticNum, instanceNum);
            }
        };
    }
}

Below is the running example:

package com.demo;

import javassist.*;

public class Test {
    public static void main(String[] args) throws NotFoundException, CannotCompileException {
        ClassPool pool = ClassPool.getDefault();
        CtClass outerClass = pool.get("com.demo.OuterClass");
        CtClass[] nestedClasses = outerClass.getNestedClasses();
        CtMethod run = nestedClasses[0].getDeclaredMethod("run");

        run.setBody("{" +
                "System.out.println(\"staticNum: \" + com.demo.OuterClass.staticNum);" +  // print: staticNum: 1
                // I tried to use the following code to access instance variables, but a compilation error occurred
                // "System.out.println(\"staticNum: \" + instanceNum);" +  // [source error] no such field: instanceNum
                // "System.out.println(\"staticNum: \" + com.demo.OuterClass.this.instanceNum);" +  // [source error] missing member name
                // "System.out.println(\"staticNum: \" + com.demo.OuterClass.access$100(com.demo.OuterClass.this));" +  // [source error] missing member name
                "}");
        nestedClasses[0].toClass();
        outerClass.toClass();
        new OuterClass().redefineMe().run();
    }
}

I want to redefine the body of the run method, but I cannot access the instance variables of the outer class in the body


Solution

  • According to the manual, Javassist does not support inner class generation, but claims to support to read and modify them:

    • Inner classes or anonymous classes are not supported. Note that this is a limitation of the compiler only. It cannot compile source code including an anonymous-class declaration. Javassist can read and modify a class file of inner/anonymous class.

    I guess the compiler support ends where you want to do use inner-class-specific syntactic sugar in source code compiled by Javassist, though. A simple workaround would be to use "sacred knowledge" as follows:

    $ javap classes/com/demo/OuterClass\$1.class
    Compiled from "OuterClass.java"
    class com.demo.OuterClass$1 implements java.lang.Runnable {
      final com.demo.OuterClass this$0;
      com.demo.OuterClass$1(com.demo.OuterClass);
      public void run();
    }
    

    Oh look, this$0! Let us try that:

    run.setBody("{" +
      "System.out.println(\"staticNum: \" + com.demo.OuterClass.staticNum);" +
      "System.out.println(\"instanceNum: \" + this$0.instanceNum);" +
      "}");
    

    Now we get the console output:

    staticNum: 1
    instanceNum: 2
    

    I do not know, how stable and reliable this workaround is across Java releases and compiler flavours.

    P.S.: If you change the anonymous inner class to a lambda, the class file looks completely different and you are lost again. I found nothing in the Javassist repository even mentioning lambdas, only a few open issues reporting problems.


    Update: I created Javassist issue #358 in order to track this.