There are many reasons JVM based languages are so popular, not only can this code be reused over different platforms without rebuilding, it’s also a mature ecosystem that has been growing for over 20 years. This, of course, means there are a lot of libraries, some of them do things that are not possible in most other languages. One of those things is the ability to create classes at runtime, add fields and/or methods to them, then instantiate and use them.
Let’s look at how Java code is executed:
Obviously, we start with the source code, which is then compiled using javac into bytecode, which can finally be executed with the Java interpreter. The interpreter constructs machine code at runtime and executes it. Most languages are compiler directly to the machine code, which is not portable across different systems and can’t be optimized at runtime like Java can.
This multi-level graph also means one thing: we can add operations between each step. For example, we can add annotation processors, to generate more code, but sometimes we want to edit existing classes, which can’t be done by annotation processing. Here’s where bytecode manipulation comes in. It operates on already compiled classes, so they’re run after compilation is done. They can be stand-alone programs, being run on individual class files, but it’s a lot more convenient to have them run as part of a plugin, in our case, a Gradle plugin. Writing Gradle plugins is a separate topic, so let’s just see how we can register a transformer in an Android plugin:
The CustomTransform here extends the com.android.build.api.transform.Transform class. It also depends on the Javassist library, which can be included with implementation ‘org.javassist:javassist:3.23.0-GA’. The basic code for it can be found here:
It basically defines a name for the transform, tells the framework that we want to transform classes, and that we want to do it on project scope. The actual magic happens in the transform method, where we get the input files, map them to their CtClass counterparts, CtClass being the Javassist version of Class. Of course, we need to get all classes first and create a ClassPool so we can actually get those:
It may seem like a lot is going on, but all we do here is find all files, ending in .class in the build directory and jar files and add them to ClassPool, to reference later. Of course to process those classes we need to get the Javassist version of the class, CtClass. This can be retrieved from the ClassPool by calling pool.getCtClass(className). We can thus process only classes we want to and skip the rest.
Javassist classes are mostly the same as regular reflection classes, with some extra methods for adding fields, function, constructors, superclasses and so on. They also include a method for saving the class to a file. So let’s say we want to add a simple print statement to all constructors. It’s rather simple, heavily resembling reflection:
That’s it! After calling addLogMethod all there’s left is to call ctClass.writeFile. If we instantiate the class now, after calling any constructor (including calls to super or this) will print “Added later”.
Note that there’s no need to manually create a ClassPool, we can also get the default one, ClassPool.getDefault(). Using the pool it’s also trivial to create a new class, all we need to do is call pool.makeClass(“ClassName”). We can then add properties and methods, as well as save it. It can also be loaded using reflection and we can also invoke methods as if the class had existed all along!