0

I have a class with,

  • a field called something,
  • a setter method called setSomething, and,
  • a method called onChange which should be called every time something is changed.

I want to be able to freely add more fields and have the same behavior for all of them.

I don't want to manually call onChange because,

  1. A lot of boilerplate,
  2. Code will be written in Kotlin so I don't want to write setter functions at all.

The ideal solution I've been able to think of has been to somehow inject the onChange call right before the return for each setter method in compile time.

I've looked at annotation processing, but apparently classes aren't actually compiled at that stage, so I'd have to generate the entire class all over again? I don't exactly understand this.

The other option seems to be writing a gradle plugin that will find the relevant class(es) and modify their bytecode.

I've actually started work on this as a pure Java project (gradle plugin is semi-done) and have been able to find the classes and inject the method call. Can't seem to successfully write the results to a class file though.

Here's what I have (using BCEL):

public class StateStoreInjector {

    public static void main(String[] args) {
        // Find all classes that extends StateStore
        Reflections reflections = new Reflections("tr.xip.statestore");
        Set<Class<? extends StateStore>> classes = reflections.getSubTypesOf(StateStore.class);
        for (Class c : classes) {
            try {
                JavaClass clazz = Repository.lookupClass(c.getName());
                JavaClass superClazz = Repository.lookupClass(StateStore.class.getName());
                if (Repository.instanceOf(clazz, superClazz)) {
                    injectInClass(clazz, superClazz);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    private static void injectInClass(JavaClass clazz, JavaClass superClazz) {
        ClassGen classGen = new ClassGen(clazz);
        ConstantPoolGen cp = classGen.getConstantPool();

        // Find the onChange method
        Method onChangeMethod = null;
        for (Method m : superClazz.getMethods()) {
            if (m.getName().equals("onChange")) {
                onChangeMethod = m;
            }
        }

        if (onChangeMethod == null) {
            throw new RuntimeException("onChange method not found");
        }

        ClassGen superClassGen = new ClassGen(superClazz);
        ConstantPoolGen superCp = superClassGen.getConstantPool();

        // Add onChange method ref to the class ConstantPool
        MethodGen onChangeMethodGen = new MethodGen(onChangeMethod, superClassGen.getClassName(), superCp);
        cp.addMethodref(onChangeMethodGen);

        // Loop through all methods to inject method invocations if applicable
        for (Method m : clazz.getMethods()) {
            // Skip methods with names shorter than 3 chars - we're looking for setters and setters would be min 4 chars
            if (m.getName().length() < 3) continue;

            // Check if the method actually starts with the keyword "set"
            boolean isSetMethod = m.getName().substring(0, 3).toUpperCase().equals("SET");
            // Get method name without the "set" keyword
            String methodName = m.getName().substring(3, m.getName().length());

            // Check that we actually have a field set by this setter - that this setter is "valid"
            boolean fieldWithSameNameExists = false;
            for (Field f : clazz.getFields()) {
                if (f.getName().toUpperCase().equals(methodName.toUpperCase())) {
                    fieldWithSameNameExists = true;
                    break;
                }
            }

            // Proceed with injection if criteria match
            Method newMethod = null;
            if (isSetMethod && fieldWithSameNameExists) {
                newMethod = injectInMethod(m, onChangeMethodGen, classGen, cp);
            }

            // Injection returned. Do we have a new/modified method? Yes? Update and write class.
            if (newMethod != null) {
                classGen.removeMethod(m);
                classGen.addMethod(newMethod);
                classGen.update();
                try {
                    String packageName = clazz.getPackageName().replace(".", "/");
                    String className = clazz.getClassName();
                    className = className.substring(className.lastIndexOf(".") + 1, className.length());
                    clazz.dump(packageName + "/" + className + "Edited.class");
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static Method injectInMethod(Method m, MethodGen onChangeMethodGen, ClassGen cg, ConstantPoolGen cp) {
        MethodGen methodGen = new MethodGen(m, cg.getClassName(), cp);
        InstructionList il = methodGen.getInstructionList();

        println(il.toString() + "pre insert ^");

        // Find the "return" instruction
        Instruction returnInstruction = null;
        for (Instruction i : il.getInstructions()) {
            if (i.getOpcode() == 177) returnInstruction = i;
        }
        // If found, insert onChange invocation instruction before the return instruction
        if (returnInstruction != null) {
            int index = cp.lookupMethodref(onChangeMethodGen); // Find the index of the onChange method in the CP
            il.insert(returnInstruction, new INVOKEVIRTUAL(index)); // Insert the new instruction

            println(il.toString() + "post insert ^");

            il.setPositions(); // Fix positions

            println(il.toString() + "post set pos ^");

            il.update();
            methodGen.update();

            return methodGen.getMethod();
        }

        return null;
    }

    private static void println(String message) {
        System.out.println(message);
    }
}

Input Java class:

public class DummyStateStore extends StateStore {
    private int id = 4321;

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

Parent Store class:

public class StateStore {

    public void onChange() {
        // notifies all subscribers 
    }
}

Output (decompiled) class file:

public class DummyStateStore extends StateStore {
    private int id = 4321;

    public DummyStateStore() {
    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return this.id;
    }
}

Log output:

   0: aload_0[42](1)
   1: iload_1[27](1)
   2: putfield[181](3) 2
   5: return[177](1)
pre insert ^
   0: aload_0[42](1)
   1: iload_1[27](1)
   2: putfield[181](3) 2
  -1: invokevirtual[182](3) 26
   5: return[177](1)
post insert ^
   0: aload_0[42](1)
   1: iload_1[27](1)
   2: putfield[181](3) 2
   5: invokevirtual[182](3) 26
   8: return[177](1)
post set pos ^

(I checked the index 26 by debugging the code and it is the correct index in the CP)

Now, the questions are:

  1. Why can't the invocation be seen in the decompiled code but it seems to be added to the instructions list? What am I missing?
  2. Where would I be exporting the modified class files in an android build for them to be included in the final apk?
xip
  • 2,475
  • 3
  • 18
  • 24
  • For annotations, why you want classes compiled? You want to add code, you precisely don't want the classes to be compiled. – m0skit0 Jan 31 '18 at 12:50
  • How would that work? I'm quite lost in that aspect. Any resources you can point me towards? – xip Jan 31 '18 at 12:52
  • I never did it, but other libraries definitely do it. This tutorial looks promising: https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657 – m0skit0 Jan 31 '18 at 12:56
  • I actually skimmed thought that article but as far as I can tell, the thing with annotation processing is that you can only _add_ new classes, not modify existing ones. I just saw something about there being a "bug" or something that allows for some level of modification though, so I'll be looking into that. To be clear - I want to add a method invocation to an _existing_ class. – xip Jan 31 '18 at 13:00
  • Did you check [this answer](https://stackoverflow.com/a/36584973/898478)? – m0skit0 Jan 31 '18 at 13:06
  • It suggests using [Lombok's method](http://notatube.blogspot.ba/2010/11/project-lombok-trick-explained.html) and upon investigation it looks like Lombok doesn't work with IntelliJ without a plugin. As I'm considering publishing the very end results of this as a library, I'd like to avoid such complexity. – xip Jan 31 '18 at 13:35

1 Answers1

0

You're trying to use reflection, but there should be no need to do so with Kotlin as you can create higher order functions (functions that take functions as inputs).

You could do something like:

class ChangeableType<T>(private var value: T, private val onChange: () -> Unit) {

    fun set(value: T) {
        this.value = value
        this.onChange.invoke()
    }
}

class MyRandomClass() {
    val something = ChangeableType(0, { System.print("Something new value: $value") })
    val anotherThing = ChangeableType("String", { System.print("Another thing new value: $value") })
}

class ConsumingClass {
    val myRandomClass = MyRandomClass()

    fun update() {
        myRandomClass.apply {
            something.set(1)
            anotherThing.set("Hello World")
        }
    }
}
Thomas Cook
  • 4,371
  • 2
  • 25
  • 42
  • Not exactly what I'm looking for. I want to be able to define a simple class with a bunch of fields, casually set like `something = "hey"` and access like `store.something`, observe anywhere, for _all_ changes using something like `store.subscribe(...)`... – xip Jan 31 '18 at 12:50