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

Tags: ,



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

Answer

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.



Source: stackoverflow