1. Overview

In this article, we will be looking at the Javasisst (Java Programming Assistant) library.

Simply put, this library makes the process of manipulating Java bytecode simpler by using a high-level API than the one in the JDK.

2. Maven Dependency

To add the Javassist library to our project we need to add javassist into our pom:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>${javaassist.version}</version>
</dependency>

<properties>
    <javaassist.version>3.21.0-GA</javaassist.version>
</properties>

3. What Is the Bytecode?

At a very high level, every Java class that is written in a plain text format and compiled to bytecode – an instruction set that can be processed by the Java Virtual Machine. The JVM translates bytecode instructions into machine level assembly instructions.

Let's say that we have a Point class:

public class Point {
    private int x;
    private int y;

    public void move(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // standard constructors/getters/setters
}

After compilation, the Point.class file containing the bytecode will be created. We can see the bytecode of that class by executing the javap command:

javap -c Point.class

This will print the following output:

public class com.baeldung.javasisst.Point {
  public com.baeldung.javasisst.Point(int, int);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iload_1
       6: putfield      #2                  // Field x:I
       9: aload_0
      10: iload_2
      11: putfield      #3                  // Field y:I
      14: return

  public void move(int, int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field x:I
       5: aload_0
       6: iload_2
       7: putfield      #3                  // Field y:I
      10: return
}

All those instructions are specified by the Java language; a large number of them are available.

Let's analyze the bytecode instructions of the move() method:

  • aload_0 instruction is loading a reference onto the stack from the local variable 0
  • iload_1 is loading an int value from the local variable 1
  • putfield is setting a field x of our object. All operations are analogical for field y
  • The last instruction is a return

Every line of Java code is compiled to bytecode with proper instructions. The Javassist library makes manipulating that bytecode relatively easy.

4. Generating a Java Class

Javassist library can be used for generating new Java class files.

Let's say that we want to generate a JavassistGeneratedClass class that implements a java.lang.Cloneable interface. We want that class to have an id field of int type*.* The ClassFile is used to create a new class file and FieldInfo is used to add a new field to a class*:*

ClassFile cf = new ClassFile(
  false, "com.baeldung.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

After we create a JavassistGeneratedClass.class we can assert that it actually has an id field:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
 
assertEquals(fields[0].getName(), "id");

5. Loading Bytecode Instructions of Class

If we want to load bytecode instructions of an already existing class method, we can get a CodeAttribute of a specific method of the class. Then we can get a CodeIterator to iterate over all bytecode instructions of that method.

Let's load all bytecode instructions of the move() method of the Point class:

ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.baeldung.javasisst.Point")
  .getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();

List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList(
  "aload_0", 
  "iload_1", 
  "putfield", 
  "aload_0", 
  "iload_2",  
  "putfield", 
  "return"));

We can see all bytecode instructions of the move() method by aggregating bytecodes to the list of operations, as shown in the assertion above.

6. Adding Fields to Existing Class Bytecode

Let's say that we want to add a field of int type to the bytecode of the existing class. We can load that class using ClassPoll and add a field into it:

ClassFile cf = ClassPool.getDefault()
  .get("com.baeldung.javasisst.Point").getClassFile();

FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);

We can use reflection to verify that id field exists on the Point class:

ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List<String> fieldsList = Stream.of(fields)
  .map(Field::getName)
  .collect(Collectors.toList());
 
assertTrue(fieldsList.contains("id"));

7. Adding Constructor to Class Bytecode

We can add a constructor to the existing class mentioned in one of the previous examples by using an addInvokespecial() method.

And we can add a parameterless constructor by invoking a method from java.lang.Object class:

ClassFile cf = ClassPool.getDefault()
  .get("com.baeldung.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);

MethodInfo minfo = new MethodInfo(
  cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);

We can check for the presence of the newly created constructor by iterating over bytecode:

CodeIterator ci = code.toCodeAttribute().iterator();
List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    operations.add(Mnemonic.OPCODE[op]);
}

assertEquals(operations,
  Arrays.asList("aload_0", "invokespecial", "return"));

8. Conclusion

In this article, we introduced the Javassist library, with the goal of making bytecode manipulation easier.

We focused on the core features and generated a class file from Java code; we also made some bytecode manipulation of an already created Java class.

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.