niceideas.ch
Technological Thoughts by Jerome Kehrli

Bytecode manipulation with Javassist for fun and profit part II: Generating toString and getter/setters using bytecode manipulation

by Jerome Kehrli


Posted on Monday Apr 24, 2017 at 10:38PM in Java


Following my first article on Bytecode manipulation with Javassist presented there: Bytecode manipulation with Javassist for fun and profit part I: Implementing a lightweight IoC container in 300 lines of code, I am here presenting another example of Bytecode manipulation with Javassist: generating toString method as well as property getters and setters with Javassist.

While the former example was oriented towards understanding how Javassist and bytecode manipulation comes in help with implementing IoC concerns, such as what is done by the spring framework of the pico IoC container, this new example is oriented towards generating boilerplate code, in a similar way to what Project Lombok is doing.
As a matter of fact, generating boilerplate code is another very sound use case for bytecode manipulation.

Boilerplate code refers to portions of code that have to be included or written in the same way in many places with little or no alteration.
The term is often used when referring to languages that are considered verbose, i.e. the programmer must write a lot of code to do minimal job. And Java is unfortunately a clear winner in this regards.
Avoiding boilerplate code is one of the main reasons (but by far not the only one of course !) why developers are moving away from Java in favor of other JVM languages such as Scala.

In addition, as a reminder, a sound understanding of the Java Bytecode and the way to manipulate it are strong prerequisites to software analytics tools, mocking libraries, profilers, etc. Bytecode manipulation is a key possibility in this regards, thanks to the JVM and the fact that bytecode is interpreted.
Traditionally, bytecode manipulation libraries suffer from complicated approaches and techniques. Javassist, however, proposes a natural, simple and efficient approach bringing bytecode manipulation possibilities to everyone.

So in this second example about Javassist we'll see how to implement typical Lombok features using Javassist, in a few dozen lines of code.

Part of this article is available as a slideshare presentation here: https://www.slideshare.net/JrmeKehrli/bytecode-manipulation-with-javassist-for-fun-and-profit.

You might want to have a look at the first article in this serie available here : Bytecode manipulation with Javassist for fun and profit part I: Implementing a lightweight IoC container in 300 lines of code.

Summary

1. Introduction / Purpose

I am giving only a few lines here and return the user to my first article on this topic for the complete introduction to bytecode manipulation.

Bytecode manipulation consists in modifying the classes - represented by bytecode - compiled by the Java compiler, at runtime. It is used extensively for instance by frameworks such as Spring (IoC) and Hibernate (ORM) to inject dynamic behaviour to Java objects at runtime.
Bytecode manipulation is traditionally difficult. Out of all the various libraries and tools to achieve it, Javassist stands out due to its natural and simple yet efficient approach to it.

2. Javassist

I am giving only a few lines here and return the user to my first article on this topic for the complete introduction to Javassist.

Quoting Wikipedia:

Javassist (Java programming assistant) is a Java library providing a means to manipulate the Java bytecode of an application.

Javassist enables Java programs to define a new class at runtime and to modify a class file when the JVM loads it. Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. Using the source-level API, programmers can edit a class file without knowledge of the specifications of the Java bytecode; the whole API is designed with only the vocabulary of the Java language. Programmers can even specify inserted bytecode in the form of Java source text; Javassist compiles it on the fly. On the other hand, the bytecode-level API allows the users to directly edit a class file as other editors.

3. Java Instrumentation framework and applications

Java 5 was the first version seeing the proper implementation of JSR-163 (Java Platform Profiling Architecture API) support including a bytecode instrumentation mechanism through the introduction of the Java Programming Language Instrumentation Services (JPLIS). At first that JSR only mentioned native (C) interfaces but evolved fast towards a pretty convenient Java API.
This was an interesting breakthrough since it allowed, with the help of an agent, to modify the content of a class bytecode inherent to the methods of a class in such a way as to modify its behavior at runtime.

The key point of the JSR-163 is JVMTI. JVMTI - or Java Virtual Machine Tool Interface - allows a program to inspect the state and to control the execution of applications running in the Java Virtual Machine. JVMTI is designed to provide an Application Programming Interface (API) for the development of tools that need access to the state of the JVM. Examples for such tools are debuggers, profilers or runtime boilerplate code generator.

The scope of this article is to focus on presenting how a boilerplate code generator can benefit from JVMTI and bytecode manipulation to achieve unprecedented comfort and easiness of programming than ever before on the JVM, unless considering a different language than Java, such as Scala or Clojure.

As a way to illustrate Boilerplate code concerns, We will focus on Project Lombok and its features and try to see how to reproduce some of them using Javassist.

3.1 Lombok

Project Lombok is a Boilerplate code generator that addresses one of the most frequent criticism against the Java Programming Language: the volume of this type of code that is found in most projects.
Boilerplate code is a term used to describe code that is repeated in many parts of an application with only slight contextual changes and with little added value.
Project Lombok reduces the need of some of the worst offenders by replacing each of them with a simple annotation. It then takes care of generating the boilerplate code at runtime

Project Lombok also integrates well in IDEs making it possible to use the usual IDE features such as refactoring and usage analysis pretty transparently.

Importantly in our context, since we will focus below on reproducing Lombok features using runtime bytecode generation, I have to mention right away that Lombok doesn't just generate Java sources or bytecode: it transforms the Abstract Syntax Tree (AST), by modifying its structure at compile-time.
The AST is a tree representation of the parsed source code, created by the compiler, similar to the DOM tree model of an XML file. By modifying (or transforming) the AST, Lombok keeps the source code trim and free of bloat, unlike plain-text code-generation.
Lombok's generated code is also visible to classes within the same compilation unit, unlike direct bytecode manipulation.
Our approach, as we will see below, using runtime bytecode manipulation shall not benefit from the same comfort and is hence less efficient than what Lombok is doing.

3.1.1 Example class

Let's see an example. Imagine the following Java POJO:

public class DataExample {

    private final String name;

    private int age;

    private double score;

    private String[] tags;
}

Typical boilerplate code involved when considering such a POJO are:

  • Getters and Setters for all private fields, making them JavaBean properties
  • A nice toString method giving the values of its properties when an object is output on the console
  • Consistent hashCode and equals methods enabling to compare and manipulate two different objects with same values
  • A default constructor without any argument (Javabean standard)
  • An all args constructor taking all values as argument to build the instance

3.1.2 Without Lombok

Without Lombok, writing all this code is a nightmare, and the simple class above becomes as follows:

import java.util.Arrays;

public class DataExample {

    private final String name;
    private int age;
    private double score;
    private String[] tags;
  
    public DataExample(String name) {
        this.name = name;
    }

    public DataExample(String name, int age, double score, String[] tags) {
        this.name = name;
        this.age = age;
        this.score = score;
        this.tags = tags;
    }
  
    public String getName() {
        return this.name;
    }
  
    void setAge(int age) {
        this.age = age;
    }
    public int getAge() {
        return this.age;
    }
  
    public void setScore(double score) {
        this.score = score;
    }
    public double getScore() {
        return this.score;
    }
  
    public String[] getTags() {
        return this.tags;
    }
    public void setTags(String[] tags) {
        this.tags = tags;
    }
  
    @Override 
    public String toString() {
        return "DataExample(" + this.getName() + 
            ", " + this.getAge() + 
            ", " + this.getScore() + 
            ", " + Arrays.deepToString(this.getTags()) + ")";
    }

    @Override 
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof DataExample)) return false;
        DataExample other = (DataExample) o;
        if (this.getName() == null ? 
                other.getName() != null : 
                !this.getName().equals(other.getName())) 
            return false;
        if (this.getAge() != other.getAge()) return false;
        if (Double.compare(this.getScore(), other.getScore()) != 0) return false;
        if (!Arrays.deepEquals(this.getTags(), other.getTags())) return false;
        return true;
    }
  
    @Override 
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        final long temp1 = Double.doubleToLongBits(this.getScore());
        result = (result*PRIME) + (this.getName() == null ? 43 : this.getName().hashCode());
        result = (result*PRIME) + this.getAge();
        result = (result*PRIME) + (int)(temp1 ^ (temp1 >>> 32));
        result = (result*PRIME) + Arrays.deepHashCode(this.getTags());
        return result;
    }
}

One needs to understand that while the features implemented by the code above are pretty useful and very important, the code itself has no added value whatsoever. Most IDEs generate this code for you using some right-click here and there. A machine can write this code for you, can you imagine this? ... And yet with java it HAS to be written. This makes no sense.

So without Lombok, for a 4 properties, 5 lines of code class ... One has to write more than 60 lines of boilerplate code, that's a ratio of [Boilerplate code / Useful Code] of more than 1600% !!!
This is the main rationality behind project Lombok.

3.1.3 With Lombok

With Lombok we can use the following set of annotations on top of the class to generate the very same boilerplate code that we had to write on our own above:

@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
@AllArgsConstructor
public class DataExample {

  private final String name;

  private int age;

  private double score;

  private String[] tags;
}

All these annotations are straightforward to understand so I won't be describing them any further.

You might want to look at the Lombok documentation to learn more about these annotations.

With Lombok, we get a much better ratio of boilerplate code concerns vs. useful code of 100% instead of more than 1600% without Lombok.

3.1.4 Lombok Apporoach (AST transformation) vs. Bytecode Manipulation vs. Code Generation

Code generation Of course there are alternatives to Lombok. For instance, most if not all IDEs enable the developer to generate these boilerplate methods in the class source file just as Lombok does it.
But generating this code in the class source file is IMHO not the right approach. At the end of the day, I believe that generated code - i.e. code that can be written by a machine - should simply not be written! If a machine can write this code then this code has no added value at all! I do not want to see such code in my class files, I do not want to be aware of it.
Lombok generates this code transparently and seamlessly. By modifying (or transforming) the AST, Lombok keeps your source code trim and free of bloat, unlike plain-text code-generation. With Lombok, I do not need to be aware of this code, it's not polluting my code coverage computation (in Sonar for instance or else) and everything behaves as if this code was actually written except that one doesn't see it and doesn't need to care about it.

Bytecode Manipulation

Bytecode manipulation is preferable over Code generation IMHO in regards to generating (at least some of the) boilerplate code. For the same reason indicated above: bytecode manipulation makes the boilerplate code transparent and avoids it polluting my source code.
It is however not as efficient as AST Transformation such as Lombok is doing. Lombok's generated code is also visible to classes within the same compilation unit, unlike direct bytecode manipulation.

For instance when it comes to generating getters and setters, Lombok makes them visible at compile time, making it possible for client code to use them without them appearing in the Java Bean class.
Using bytecode manipulation to generate getters and setters, they become invisible to the compiler and cannot be used by another class, except using runtime reflection, which may be fine for some use cases (hibernate, etc) but not for most of them.

AST Transformation

More information on Lombok's internal is availanle there: Custom AST transformations with Project Lombok.

3.1.5 Just a note on concerns

I sometime hear (... read) arguments online against the use of Lombok. Most of the time, people against Lombok complain about the magic added by Lombok.

It is true that Lombok adds a lot of magic to the application. But that magic is related to boilerplate code with no added value and easy to debug and understand in case of any doubt. In addition, it is very well recognized by most common IDEs such as eclipse and IDEA using well known or even official plugins.

3.2 Java agents and the linkage problem

Javassist cannot modify a class after it has been loaded by a classloader ... as far as this classloader is concerned.
Whenever one tries to modify a class already loaded by the referenced classloader, that attempt to call pool.makeClass( ... ) will fail and complain that class is frozen (i.e. already created via toClass().
Being able to do that would require to unload the class first from the reference Classloader.

The problem here is that one cannot unload a single class from a ClassLoader. A class may be unloaded if it and its ClassLoader became unreachable but since every class refers to its loader that implies that all classes loaded by this loader must have to become unreachable too.
Of course one can (re-)create the class using a different ClassLoader but that would require to make the whole program use that new Classloader and this becomes fairly complicated.
At the end of the day, that would well require reloading the whole application and initializing everything all over again. This makes no sense.

Let's just accept here that a class cannot be changed by Javassist once it has been already loaded by the Classloader.

3.2.1 Overcoming the Linkage problem with Java Agents

The only (easy) way to overcome this problem is to change the class implementation using bytecode manipulation before the class is loaded by any Classloader. And happily, as often in the Java World, the JVM provides a mechanism for this, the JPLIS - Java Programming Language Instrumentation Services - and the concept of Java Agent.

In its essence, a Java agent is a regular Java class which follows a set of strict conventions. The agent class must implement a public static void premain(String agentArgs, Instrumentation inst) method which becomes an agent entry point (similar to the main method for regular Java applications).

Once the Java Virtual Machine (JVM) has initialized, each such premain(String agentArgs, Instrumentation inst) method of every agent will be called in the order the agents were specified on JVM start. When this initialization step is done, the real Java application main method will be called.

The instrumentation capabilities of Java agents are truly unlimited. Most noticeable one is the ability to redefine classes at run-time. The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance.

Please notice that re-transformed or redefined class bytecode is not checked, verified and installed just after the transformations or redefinitions have been applied. If the resulting bytecode is erroneous or not correct, the exception will be thrown and that may crash JVM completely.

3.2.2 ClassFileTransformer

A Java agent premain method takes the Instrumentation entry point - class java.lang.instrument.Instrumentation - as argument.

The Instrumentation entry point provides several commodity methods to check the possibilities of the JVM but the most important API of the java.lang.instrument.Instrumentation class is the method void addTransformer(ClassFileTransformer transformer); that enable the developer to register several java.lang.instrument.ClassFileTransformer.

The java.lang.instrument.ClassFileTransformer interface defines one single method byte[] transform(...) that is responsible to apply transformations (as far as complete rewriting if required) the java classes being loaded by the JVM.
The transform(...) method is called for each and every class being loaded by a classloader. Both the class being loaded and the classloader actually loading it as well as other information are given in argument.

The transform(...) method is the ideal place where bytecode manipulation libraries can be used to modify classes just before they are loaded by the classloader.

Java Agent Behaviour
(Source : http://www.barcelonajug.org/2015/04/java-agents.html)

3.2.3 Caution

As a sidenote, the Java agent class may also have a public static void agentmain(String agentArgs, Instrumentation inst) method which is used when the agent is started after JVM startup.

A common practice when developing agents is to implement both the agentmain and premain methods and delegate one to the other. See implementation of Java Agent of BCG below.

3.2.4 Simple Example

package ch.niceideas.common.agent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

public class ClassLoadingLoggingAgent {

    public static void premain(String agentArgument, Instrumentation instrumentation){     
        System.out.println("Hello, Agent [ " + agentArgument + " ]");

        instrumentation.addTransformer (new ClassFileTransformer() {

            @Override
            public byte[] transform(
                    ClassLoader loader, 
                    String className, 
                    Class classBeingRedefined, 
                    ProtectionDomain protectionDomain, 
                    byte[] classfileBuffer) throws IllegalClassFormatException {

                // Transform is called just before class loading occurs :-)
                System.out.println("Class being loaded : " + className);

                // No transformation ...
                return classfileBuffer;
            }
        });
    }
}

Let's now see how to invoke a simple program using this simple agent.

3.2.5 Invoking the Agent

When running from the command line, the Java agent could be passed to JVM instance using -javaagent argument which has following semantic -javaagent:<path-to-jar>[=options].

A java agent needs to be packaged in a jar file and that jar file needs to have a specific and proper MANIFEST.MF file indicating the class containing the premain method.

A proper manifest file for the agent above should be packages within the jar archive containing the agent classes under META-INF/MANIFEST.MF and would be as follows:

Manifest-Version: 1.0
Premain-Class: ch.niceideas.common.agent.ClassLoadingLoggingAgent

Now let's imagine we invoke our agent on a simple program defined as follows:

package ch.niceideas.common.enhancer;

public class TestMain {

    public static void main (String args[]) {
        System.out.println ("Program Main");
    }
}

Sample result on this simple program is as follows:

badtrash@badbook:/data/work/niceideas-commons/target/test-classes$ java \
    -javaagent:/home/badtrash/ClassLoadingLoggingAgent.jar=007 \
    ch.niceideas.common.enhancer.TestMain

Hello, Agent [ 007 ]
Class being loaded : java/lang/invoke/MethodHandleImpl
Class being loaded : java/lang/invoke/MethodHandleImpl$1
Class being loaded : java/lang/invoke/MethodHandleImpl$2
Class being loaded : java/util/function/Function
Class being loaded : java/lang/invoke/MethodHandleImpl$3
Class being loaded : java/lang/invoke/MethodHandleImpl$4
Class being loaded : java/lang/ClassValue
Class being loaded : java/lang/ClassValue$Entry
Class being loaded : java/lang/ClassValue$Identity
Class being loaded : java/lang/ClassValue$Version
Class being loaded : java/lang/invoke/MemberName$Factory
Class being loaded : java/lang/invoke/MethodHandleStatics
Class being loaded : java/lang/invoke/MethodHandleStatics$1
Class being loaded : sun/launcher/LauncherHelper
Class being loaded : java/util/concurrent/ConcurrentHashMap$ForwardingNode
Class being loaded : sun/misc/URLClassPath$FileLoader$1
Class being loaded : java/lang/Package
Class being loaded : java/io/FileInputStream$1
Class being loaded : ch/niceideas/common/enhancer/TestMain
Class being loaded : sun/launcher/LauncherHelper$FXHelper
Class being loaded : java/lang/Class$MethodArray
Class being loaded : java/lang/Void
Program Main
Class being loaded : java/lang/Shutdown
Class being loaded : java/lang/Shutdown$Lock

3.2.6 Workaround

As a sidenote, and to conclude this section, let's just mention that using a java agent to inject behaviour at runtime using bytecode manipulation is not always a requirement, it depends eventually on the use case.
A pretty common approach favored over java agents usage is the subclassing approach. It consists in defining a new class as a subclass of the class to be enhanced and injecting the new behaviour to that subclass instead.
This is a pretty straightforward approach and prevents the usage of a java agent since we don't care whether or not the initial class has already be loaded. Since we define a new class, the subclass, we're good to go no matter what happens with the initial class.

I have given an example of this approach in my previous article as described here.

In the case of boilerplate code generation such as done by Lombok, using an agent is pretty much the only way. The Value Objects or Java Beans enhanced this way can well be used later by the running program for ORM concerns or IoC concerns. These other frameworks, such as hibernate or spring, overy often use the subclassing approach to inject their own behaviour.
If a programmer attempts to use the subclass trick to inject his own behaviour and then use his class with hibernate for instance, his changes will likely be ignored by hibernate or spring that will generate their own subclass (using CGLIB or Javassist) that can conflict the developer's own subclass. In this case enhancing the class itself is a way simpler approach.

Finally, using a java agent is a convenient way to avoid situations where the developer attempts to enhance a class that has already been loaded by the classloader. But that is not necessarily always required and the developer can well choose to implement his own control over the application lifecycle to ensure he has the chance to modify classes before the application attempts to load them.
But that is impossible when using annotations of course and hence frameworks such as Lombok using annotations extensively have no other choice than using a java agent.

4. BCG: a simple approach for generating boilerplate code using Javassist

Now back on the Javassist topic.

The purpose of this article is to give a second example of a sound Javassist use case: the generation of boilerplate code using bytecode manipulation, just as project Lombok is doing.
In fact, I will present here the BCG tool that mimic Lombok and re-implement two features of the Lombok feature set.

I am presenting here the few dozen lines of code on the BCG tool - BCG for Boilerplate Code Generator.
BCG is a simple tool that uses Javassist and implement a Java agent to key Lombok features:

  1. toString() method generation
  2. property getters and setters generation

Note that BCG is not a production tool or anything like it, it is really just a Javassist example and intended to demonstrate how straightforward, simple and efficient it would be to re-implement Lombok features using Javassist ... Should one want to do that, which is not likely since Lombok is working so cool and so easily extendable.

As mentionned above, we will only be mimicking project Lombok here using bytecode manipulation. We are not implementing these features the same way Lombok is doing. Lombok is working at compile-time using AST Transformation. We will be working at runtime using bytecode manipulation.

4.1 Principle

We want to be able to implement transformers that take care of performing one specific modification to target classes and activated by the presence of one specific annotation on these classes.

The key idea is to implement a Java Agent that analyze each and every class just before is loaded by the classloader and verifies if this class needs to be transformed.
We want to implement Transformers that recognize classes declaring a specific annotation and proceed with the transformation of these classes.
We want the system to be easily extendable with new transformers.

4.2 Design

The design of BCG is as follows:

Boilerplate code generator design

Principal components are as follows:

  • EnhancerAgent : This is a JVM Agent that implements classes transformation from recognized annotations.
    The EnhancerAgent is called before the application starts and registers a java.lang.instrument.ClassFileTransformer that enhances classes declaring specific annotations before they are loaded by classloader(s).
    The java.lang.instrument.ClassFileTransformer here is a simple anonymous adapter.
  • ClassTransformer : This is an interface implemented by actual Class Transformers. A Class Transformer transforms Java classes declaring the recognized annotation(s) using Javassist.
  • AbstractTransformer : This is the base class for all Transformers. It provides commodity routines for ClassTransformers and simplifies registration API.

Then all actual transformers extend AbstractTransformer and simply declare the annotation they recognize.

4.3 Implementation

The source code of all classes and interfaces from the design above is given below.

(In all snippets of code from now on, I will be coloring relevant Javassist API calls in dark red)

4.3.1 The code of the Agent

[Class EnhancerAgent]

package ch.niceideas.common.enhancer;

import ch.niceideas.common.enhancer.impls.CountInstanceTransformer;
import ch.niceideas.common.enhancer.impls.DataTransformer;
import ch.niceideas.common.enhancer.impls.ToStringTransformer;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;

/**
 * This is a JVM Agent that implements classes transformation from recognized
 * annotations.
 * <p />
 *
 * The EnhancerAgent is called before the application starts and registers a
 * <code>java.lang.instrument.ClassFileTransformer</code> that enhances
 * classes declaring specific annotations before they are loaded
 * by classloader(s).
 * <p />
 *
 * The <code>java.lang.instrument.ClassFileTransformer</code> here is a simple
 * anonymous adapter.
 */
public class EnhancerAgent {

    private static ClassTransformer[] transformers = null;

    // for now I don't have any better way than declaring all transformers here
    static {
        transformers = new ClassTransformer[] {
                new CountInstanceTransformer(),
                new ToStringTransformer(),
                new DataTransformer()
        };
    }

    // Java Agent API
    public static void premain(String agentArgs, Instrumentation inst) {
        agentmain (agentArgs, inst);
    }

    // API used when agent invoked after JVM Startup
    public static void agentmain(String agentArgs, Instrumentation inst) {

        inst.addTransformer(new ClassFileTransformer() {

            @Override
            public byte[] transform (
                    ClassLoader loader,
                    String className,
                    Class classBeingRedefined,
                    ProtectionDomain protectionDomain,
                    byte[] classfileBuffer)
                    throws IllegalClassFormatException {

                // Can return null if no transformation is performed
                byte[] transformedClass = null;

                CtClass cl = null;
                ClassPool pool = ClassPool.getDefault();
                try {
                    cl = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));

                    for (ClassTransformer transformer : transformers) {

                        if (transformer.accepts (cl)) {
                            transformer.transform (cl);
                            System.out.println ("Transformed class " + cl.getName()
                                    + " with " + transformer.getClass().getSimpleName());
                        }
                    }

                    // Generate changed bytecode
                    transformedClass = cl.toBytecode();

                } catch (IOException | CannotCompileException e) {
                    e.printStackTrace();

                } finally {
                    if (cl != null) {
                        cl.detach();
                    }
                }

                return transformedClass;
            }
        });
    }
}

4.3.2 Interface ClassTransformer

Transformers implement this interface:

[Class ClassTransformer]

package ch.niceideas.common.enhancer;

import javassist.CtClass;

/**
 * A class Transformer transforms Java classes declaring the recognized
 * annotation(s) using Javassist.
 */
public interface ClassTransformer {

    /**
     * Used by the EnhancerAgent to know whether this class accepts the supported anotation
     *
     * @param cl the class to test
     * @return true if the passed annotation is accepted
     */
    boolean accepts(CtClass cl);

    /**
     * Proceed with the transformation of the javassist loaded class given as argument
     *
     * @param cl the javassist loaded class to be transformed
     */
    void transform(CtClass cl);

}

4.3.3 Common Abstraction

And an abstract class provides some binding commodities to transformers

[Class AbstractTransformer]

package ch.niceideas.common.enhancer.impls;

import ch.niceideas.common.enhancer.ClassTransformer;
import javassist.CtClass;

import java.lang.annotation.Annotation;

/**
 * Base class for ClassTransformers.
 * <br />
 * Provides commodity routines for ClassTransformers and simplifies registration API.
 */
public abstract class AbstractTransformer implements ClassTransformer {

    @Override
    public final boolean accepts(CtClass cl) {
        return cl.hasAnnotation(getAnnotationClass());
    }

    /**
     * Classes that wants to be transformed by this transformer needs to declare
     * this annotation.
     *
     * @return the type of the annotation accepted by this transformer.
     */
    protected abstract Class<? extends Annotation> getAnnotationClass();

    @Override
    public abstract void transform(CtClass cl);
}

4.3.4 The set of Class Transformers

A first Class Transformer : outputs the count of instances of classes declaring the @CountInstance annotation.

[Class CountInstanceTransformer]

package ch.niceideas.common.enhancer.impls;

import ch.niceideas.common.enhancer.ClassTransformer;
import ch.niceideas.common.enhancer.annotations.CountInstance;
import javassist.CannotCompileException;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.CtField;

import java.lang.annotation.Annotation;

/**
 * This transformer accepts classe declaring the "@CountInstance" annotation.
 * 
* It enhances the class with an instanceCounter and outputs the value of ths * instanceCounter everytime an instance is built. */
public class CountInstanceTransformer extends AbstractTransformer implements ClassTransformer { @Override protected Class<? extends Annotation> getAnnotationClass() { return CountInstance.class; } @Override public void transform(CtClass cl) { try { if (!cl.isInterface()) { // Add a static field in the class CtField field = CtField.make("private static long _instanceCount;", cl); cl.addField(field); CtBehavior[] constructors = cl.getDeclaredConstructors(); for (int i = 0; i < constructors.length; i++) { // Increment counter and output it constructors[i].insertAfter("_instanceCount++;"); constructors[i].insertAfter("System.out.println(\"" + cl.getName() + " : \" + _instanceCount);"); } } } catch (CannotCompileException e) { e.printStackTrace(); throw new RuntimeException (e); } } }

The CountInstanceTransformer accepts classes declaring the CountInstance annotation :

package ch.niceideas.common.enhancer.annotations;

/**
 * Classes declaring this annotation will have an instancecounter which value is
 * output everytime an instance is constructed
 */
public @interface CountInstance {
}

Second Class Transformer: generates getters and setters for classes declaring the @Data annotation.

[Class DataTransformer]

package ch.niceideas.common.enhancer.impls;

import ch.niceideas.common.enhancer.ClassTransformer;
import ch.niceideas.common.enhancer.annotations.Data;
import javassist.*;

import java.lang.annotation.Annotation;

/**
 * This transformer accepts classes declaring the "@Data" annotation.
 * <br />
 * It generates a getters and setters dynamically for every field of the class
 * if they do not already exist
 */
public class DataTransformer extends AbstractTransformer implements ClassTransformer {

    @Override
    protected Class<? extends Annotation> getAnnotationClass() {
        return Data.class;
    }

    @Override
    public void transform(CtClass cl) {
        try {
            if (!cl.isInterface()) {

                for (CtField field : cl.getDeclaredFields()) {

                    String camelCaseField = field.getName().substring(0, 1).toUpperCase()
                            + field.getName().substring(1);

                    if (!hasMethod("get" + camelCaseField, cl)) {
                        cl.addMethod(CtNewMethod.getter("get" + camelCaseField, field));
                    }

                    if (!hasMethod("set" + camelCaseField, cl)) {
                        cl.addMethod(CtNewMethod.setter("set" + camelCaseField, field));
                    }
                }
            }

        } catch (CannotCompileException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /** javassist has unfortunately no hasMethod API */
    private static boolean hasMethod (String methodName, CtClass cl) {
        try {
            cl.getDeclaredMethod(methodName);
            return true;
        } catch (NotFoundException e) {
            return false;
        }
    }
}

The DataTransformer accepts classes declaring the Data annotation :

package ch.niceideas.common.enhancer.annotations;

/**
 * Classes declaring this annotation will have getters and setters automatically
 * generated
 */
public @interface Data {
}

Third Class Transformer : generates the toString method for classes declaring the @ToString annotation.

[Class DataTransformer]

package ch.niceideas.common.enhancer.impls;

import ch.niceideas.common.enhancer.ClassTransformer;
import ch.niceideas.common.enhancer.annotations.ToString;
import javassist.*;

import java.lang.annotation.Annotation;

/**
 * This transformer accepts classes declaring the "@ToString" annotation.
 * <br />
 * It generates a toString method dynamically. The toString method is generated using
 * bytecode manipulation and avoids reflection.
 */
public class ToStringTransformer extends AbstractTransformer implements ClassTransformer {

    @Override
    protected Class<? extends Annotation> getAnnotationClass() {
        return ToString.class;
    }

    @Override
    public void transform(CtClass cl) {
        try {
            if (!cl.isInterface()) {

                StringBuilder bb = new StringBuilder("{\n");
                bb.append("    StringBuilder sb = new StringBuilder(\"" 
                        + cl.getName() + "\");\n");
                bb.append("    sb.append(\"[\");\n");

                for (CtField field : cl.getDeclaredFields()) {
                    field.setModifiers(Modifier.PUBLIC); // hacky hack

                    bb.append("    sb.append(\"" + field.getName() + "\");\n");
                    bb.append("    sb.append(\"=\");\n");
                    bb.append("    sb.append(this." + field.getName() + ");\n");
                    bb.append("    sb.append(\" \");\n");
                }

                bb.append("    sb.append(\"]\");\n");
                bb.append("    return sb.toString();\n");
                bb.append("}");

                try {
                    CtMethod toStringMethod = cl.getDeclaredMethod("toString");
                    toStringMethod.setBody(bb.toString());

                } catch (NotFoundException e) {

                    CtMethod newMethod = CtNewMethod.make("public String toString() \n"
                            + bb.toString(), cl);
                    cl.addMethod(newMethod);
                }
            }

        } catch (CannotCompileException e) {
            e.printStackTrace();
            throw new RuntimeException (e);
        }
    }
}

The ToStringTransformer accepts classes declaring the ToString annotation :

package ch.niceideas.common.enhancer.annotations;

/**
 * Classes declaring this annotation will have a toString method generated
 * automagically
 */
public @interface ToString {
}

4.3.5 Test Class Example

A test class for the DataTransformer for instance, with the usage of a nice library to test agents: ElectronicArts AgentLoader.

EA Agent Loader is a collection of utilities for java agent developers. It allows programmers to write and test their java agents using dynamic agent loading (without using the -javaagent jvm parameter).

package ch.niceideas.common.enhancer;

import ch.niceideas.common.enhancer.testData.TestData;
import com.ea.agentloader.AgentLoader;
import junit.framework.TestCase;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.lang.reflect.Method;

public class DataTest extends TestCase{

    private static final Logger logger = Logger.getLogger(DataTest.class);

    @Before
    public void setUp() throws Exception {
        AgentLoader.loadAgentClass(EnhancerAgent.class.getName(), "");
    }

    @Test
    public void testDataTransformer() throws Exception {

        TestData testData = new TestData();

        Method getI = TestData.class.getDeclaredMethod("getI");
        assertEquals (0, getI.invoke(testData));

        Method getMyString = TestData.class.getDeclaredMethod("getMyString");
        assertEquals ("abc", getMyString.invoke(testData));

        Method getValue = TestData.class.getDeclaredMethod("getValue");
        assertEquals (-1L, getValue.invoke(testData));

        Method setI = TestData.class.getDeclaredMethod("setI", int.class);
        setI.invoke(testData, 9);
        assertEquals (9, getI.invoke(testData));

        Method setMyString = TestData.class.getDeclaredMethod("setMyString", String.class);
        setMyString.invoke(testData, "xyz");
        assertEquals ("xyz", getMyString.invoke(testData));

        Method setValue = TestData.class.getDeclaredMethod("setValue", long.class);
        setValue.invoke(testData, 999L);
        assertEquals (999L, getValue.invoke(testData));
    }
}

This test case uses the following test data :

package ch.niceideas.common.enhancer.testData;

import ch.niceideas.common.enhancer.annotations.Data;

/**
 * a test Data for the DataTest test case
 */
@Data
public class TestData {

    private int i = 0;

    private String myString = "abc";

    private long value = -1;
}

5. Conclusion

Again. Bytecode manipulation opens a whole lot of new possibilities for the JVM and is key to address one of the biggest weakness of the JVM: the overwhelming verbosity of the language. The approaches and techniques presented above are extensively used by so many libraries and frameworks that have become the facto standard nowadays: AspectJ, Spring, Hibernate, Jprofiler, etc.
For that reason, and because it's really a lot of fun, one might find bytecode manipulation a pretty valuable mechanism to master.

In addition, even though Lombok uses for very good reasons a different technique (AST Tranformation), I find it astonishing to see how bytecode manipulation enables a developer to mimic its features in so few lines of code.

One can use bytecode manipulation to perform many tasks that would be difficult or impossible to do otherwise, and once one learns it, the sky is the limit.

Javassist put this power in hands of every Java developer in a simple, intuitive and efficient way.

You might want to have a look at the first article in this serie available here : Bytecode manipulation with Javassist for fun and profit part I: Implementing a lightweight IoC container in 300 lines of code.

Part of this article is available as a slideshare presentation here: https://www.slideshare.net/JrmeKehrli/bytecode-manipulation-with-javassist-for-fun-and-profit.



No one has commented yet.

Leave a Comment

HTML Syntax: Allowed