Skip to content

Latest commit

 

History

History
852 lines (668 loc) · 32.1 KB

USAGE.adoc

File metadata and controls

852 lines (668 loc) · 32.1 KB

Basic Usage

Creating a Class

In order to generate bytecode of a new class, we need to create a ClassCreator instance. We can start with the convenient ClassCreator.Builder.

ClassCreator creator = ClassCreator.builder() (1)
                             .classOutput(cl) (2)
                             .className("com.MyTest") (3)
                             .build() (4)
  1. Create a new ClassCreator.Builder instance.

  2. Set the ClassOutput that is used to write the bytecode of the class.

  3. Specify the fully-qualified class name.

  4. Build the ClassCreator instance.

By default, the generated class is public and synthetic (as defined by the Java Language Specification). If you need to make it final then use the ClassCreator.Builder#setFinal() method.

The ClassCreator.Builder#superClass() and ClassCreator.Builder#interfaces() methods can be used to specify the superclass and implemented interfaces.

Adding Fields

Once we have a ClassCreator instance, we can start adding fields and methods. Fields can be added via the FieldCreator interface returned from the ClassCreator#getFieldCreator() methods. By default, the generated field is private. You can use the FieldCreator#setModifiers() method to control the modifiers of the generated field.

FieldCreator Example
import org.objectweb.asm.Opcodes.ACC_FINAL;
import org.objectweb.asm.Opcodes.ACC_PROTECTED;

void addField(ClassCreator fooClazz) {
    FieldCreator myField = fooClazz.getFieldCreator("score", Integer.class); (1)
    myField.setModifiers(ACC_FINAL | ACC_PROTECTED); (2)
}
  1. Create a field score of type Integer.

  2. Make the field protected final.

Adding Methods

A method can be added through a MethodCreator interface returned from the ClassCreator#getMethodCreator() methods. By default, the generated method is public. You can use the MethodCreator#setModifiers() method to control the modifiers of the generated method.

The bytecode of the method is generated via BytecodeCreator. See Generating Method Bytecode for more information about how to generate some of the most common operations.

MethodCreator Example
import org.objectweb.asm.Opcodes.ACC_PRIVATE;

void addMethod(ClassCreator fooClazz) {
    MethodCreator alwaysReturnFalse = fooClazz.getMethodCreator("alwaysReturnFalse", boolean.class); (1)
    alwaysReturnFalse.setModifiers(ACC_PRIVATE);
    // Note that MethodCreator also implements BytecodeCreator
    alwaysReturnFalse.returnValue(alwaysReturnFalse.load(false)); (2) (3)
}
  1. Create a method with the following signature private boolean alwaysReturnFalse().

  2. A MethodCreator must always define a return statement. Use returnValue(null) for void methods.

  3. This is an equivalent of return false;.

Generating Method Bytecode

The BytecodeCreator interface allows you to generate some of the most common bytecode operations. It’s not intended to cover all the functionality provided by the JVM and the Java language though. Most operations return and accept a ResultHandle. Simply put a result handle represents a value on the stack. It could be a result of a method invocation, a method argument, a constant, a result of a read from a field, etc. It’s immutable. AssignableResultHandle, on the other hand, is more like a local variable that can be assigned via BytecodeCreator.assign().

Working with Fields

An instance field can be read with the BytecodeCreator#readInstanceField() method. A static field can be read with the BytecodeCreator#readStaticField() method. And the corresponding writeInstanceField() and writeStaticField() methods are used to write a field value.

Field Operations Example
void fieldOperations(MethodCreator method, ResultHandle foo) {
    // Boolean booleanTrue = Boolean.TRUE;
    FieldDescriptor trueField = FieldDescriptor.of(Boolean.class, "TRUE", Boolean.class);
    ResultHandle booleanTrue = method.readStaticField(trueField);
    // foo.bar = booleanTrue;
    FieldDescriptor barField = FieldDescriptor.of(Foo.class, "bar", Boolean.class);
    method.writeInstanceField(fooField, foo, booleanTrue);
}

Invoking Methods

The JVM instruction set has several bytecode instructions for a method invocation. Gizmo covers invokestatic, invokeinterface, invokevirtual and invokespecial.

BytecodeCreator#invokeStaticMethod() is used to invoke static methods.

Note
If you need to invoke a static method of an interface then BytecodeCreator#invokeStaticInterfaceMethod() must be used.
Boolean.parseBoolean(String) Invocation Example
void invokeParseBoolean(MethodCreator method) {
    // String val = "true";
    ResultHandle str = method.load("true");
    MethodDescriptor parseBoolean = MethodDescriptor.ofMethod(Boolean.class, "parseBoolean", boolean.class, String.class);
    // boolean result = Boolean.parseBoolean(val);
    ResultHandle result = method.invokeStaticMethod(parseBoolean, str);
    // System.out.printl(val)
    Gizmo.systemOutPrintln(method, result);
}

BytecodeCreator#invokeInterfaceMethod() is used to invoke a method of an interface.

Collection.size() Invocation Example
void invokeSize(MethodCreator method, ResultHandle someCollection) {
    MethodDescriptor size = MethodDescriptor.ofMethod(Collection.class, "size", int.class);
    // System.out.printl(someCollection.size())
    Gizmo.systemOutPrintln(method, method.invokeInterfaceMethod(size, someCollection));
}

BytecodeCreator#invokeVirtualMethod() is used to invoke a virtual method, i.e. a public, package-private and protected methods of a class.

String.toLowerCase() Invocation Example
void invokeToLowerCase(MethodCreator method) {
    // String val = "HELLO";
    ResultHandle str = method.load("HELLO");
    MethodDescriptor toLowerCase = MethodDescriptor.ofMethod(String.class, "toLowerCase", String.class);
    // String result = str.toLowerCase();
    ResultHandle result = method.invokeVirtualMethod(toLowerCase, str);
    // System.out.printl(result)
    Gizmo.systemOutPrintln(method, result);
}

BytecodeCreator#invokeSpecialMethod() is used to invoke private instance methods, superclass methods or constructors.

Private Method Invocation Example
void invokeSuperToString(MethodCreator method) {
    MethodDescriptor myPrivateMethod = MethodDescriptor.of(Foo.class,"privateMethod", String.class);
    // String result = privateMethod();
    ResultHandle result = method.invokeSpecialMethod(myPrivateMethod, method.getThis()); (1)
    // System.out.printl(result)
    Gizmo.systemOutPrintln(method, result);
}
  1. BytecodeCreator.getThis() represents the current object

Loops

Sometimes you need to generate the bytecode to iterate over a collection of elements. There are two constructs that could be useful: ForEachLoop and WhileLoop. In the following snippet we’re going to generate a bytecode to iterate over all elements of a java.lang.Iterable instance.

ForEachLoop Example
void iterate(MethodCreator method, ResultHandle iterable) {
    // for (Object element : iterable) {
    //   System.out.println(element);
    // }
    ForEachLoop loop = method.forEach(iterable);
    BytecodeCreator block = loop.block();
    Gizmo.systemOutPrintln(block, loop.element());
}
Note
Unlike the for-each in Java the ForEachLoop does not support arrays, i.e. it’s only possible to iterate over an instance of Iterable.

And the next snippet is using the WhileLoop and java.util.Iterator instead.

WhileLoop Example
import io.quarkus.gizmo.Gizmo.JdkIterator.HAS_NEXT;
import io.quarkus.gizmo.Gizmo.JdkIterator.NEXT;

void iterate(MethodCreator method, ResultHandle iterator) {
    // while (iterator.hasNext()) {
    //   System.out.println(iterator.next());
    // }
    WhileLoop loop = method.whileLoop(bc -> bc.invokeInterfaceMethod(HAS_NEXT, iterator));
    BytecodeCreator block = loop.block();
    Gizmo.systemOutPrintln(block, block.invokeInterfaceMethod(NEXT, iterator));
}

If Statements

Gizmo provides some basic control flow constructs. The BytecodeCreator declares several methods that start with the if prefix. A typical example is the ifTrue() method which can be used to generate a simple if-then bytecode.

ifTrue() Example
void ifTrue(MethodCreator method, ResultHandle value) {
   // if (value) {
   //  System.out.println("Value is true");
   // }
   BranchResult result = method.ifTrue(value);
   BytecodeCreator trueBranch = result.trueBranch();
   Gizmo.systemOutPrintln(trueBranch, trueBranch.load("Value is true"));
}
Note
There are other variants such as ifNull() and ifFalse().

If you need a more complex if-then-else bytecode then you can try the ifThenElse() method and the returned IfThenElse construct.

ifThenElse() Example
void ifThenElse(MethodCreator method, ResultHandle value) {
    // String val;
    // if (val.equals("foo")) {
    //   val = "FOO";
    // } else if (val.equals("bar")) {
    //   val = "BAR!";
    // } else if (val.equals("baz")) {
    //   var = "BAZ!";
    // } else {
    //   val = "OTHER!";
    // }
    IfThenElse ifValue = method.ifThenElse(Gizmo.equals(method, value, method.load("foo")));

    BytecodeCreator ifFooNext = ifValue.then();
    ifFooNext.assign(ret, ifFooNext.load("FOO!"));

    BytecodeCreator ifBar = ifValue.elseIf(b -> Gizmo.equals(b, value, b.load("bar")));
    ifBar.assign(ret, ifBar.load("BAR!"));

    BytecodeCreator ifBaz = ifValue.elseIf(b -> Gizmo.equals(b, value, b.load("baz")));
    ifBaz.assign(ret, ifBaz.load("BAZ!"));

    BytecodeCreator elseThen = ifValue.elseThen();
    elseThen.assign(ret, elseThen.load("OTHER!"));
}

Switch Constructs

Gizmo has two constructs to generate the bytecode output similar to Java switch statement/expressions. The BytecodeCreator#stringSwitch() method creates a new Switch construct for a String value. While the BytecodeCreator#enumSwitch() method creates a new Switch construct for an enum value.

By default, the fall through is disabled. A case block is treated as a switch rule block; i.e. it’s not necessary to add the break statement to prevent the fall through.

Fall Through Disabled Example
// String ret;
// switch (arg) {
//     case "boom", "foo" -> ret = "foooboom";
//     case "bar" -> ret = "barr";
//     case "baz" -> ret = "bazz";
//     default -> ret = null;
// }
// return ret;
StringSwitch s = method.stringSwitch(strResultHandle);
s.caseOf(List.of("boom", "foo"), bc -> {
   bc.assign(ret, bc.load("foooboom"));
});
s.caseOf("bar", bc -> {
   bc.assign(ret, bc.load("barr"));
});
s.caseOf("baz", bc -> {
   bc.assign(ret, bc.load("bazz"));
});
s.defaultCase(bc -> bc.assign(ret, bc.loadNull()));

However, if fall through is enabled then a case block is treated as a labeled statement group; i.e. it’s necessary to add the break statement to prevent the fall through.

Fall Through Enabled Example
// String ret;
// switch (arg) {
//     case "boom":
//     case "foo":
//          ret = "fooo";
//          break;
//      case "bar":
//          ret = "barr"
//      case "baz"
//          ret = "bazz";
//          break;
//      default:
//          ret = null;
// }
// return ret;
StringSwitch s = method.stringSwitch(strResultHandle);
s.fallThrough();
s.caseOf(List.of("boom", "foo"), bc -> {
   bc.assign(ret, bc.load("fooo"));
   s.doBreak(bc);
});
s.caseOf("bar", bc -> {
   bc.assign(ret, bc.load("barr"));
});
s.caseOf("baz", bc -> {
   bc.assign(ret, bc.load("bazz"));
   s.doBreak(bc);
});
s.defaultCase(bc -> bc.assign(ret, bc.loadNull()));

Type conversions (casting)

There are 3 methods for type conversions:

  • checkCast()

  • convertPrimitive()

  • smartCast()

The checkCast() method emits the checkcast instruction, which performs a dynamic type check and a reference conversion. If the value that is supposed to be converted is statically known to be of a primitive type, the checkCast() method emits a boxing conversion first, before the type conversion.

The convertPrimitive() method emits primitive conversion instructions (i2l, d2i etc.). If the value that is supposed to be converted is statically known to be of a primitive wrapper class, the convertPrimitive() method emits an unboxing conversion first, before the type conversion.

The smartCast() method emits a sequence of instructions to cast the value to the given target type, using boxing, unboxing, primitive and reference conversions. For example, it allows casting java.lang.Integer to java.lang.Long by an unboxing conversion, followed by a primitive conversion, followed by a boxing conversion. This method is not equivalent to the casting conversion described by Java Language Specification; it is a superset.

High-level Utilities

The Gizmo class contains many utilities for generating common code sequences.

Common Methods

Gizmo.toString(BytecodeCreator target, ResultHandle obj) generates an invocation of obj.toString() into target. It returns a ResultHandle of type java.lang.String. Note that this code sequence fails at runtime with NullPointerException when obj represents the null reference.

Gizmo.equals(BytecodeCreator target, ResultHandle obj1, ResultHandle obj2) generates an invocation of obj1.equals(obj2) into target. It returns a ResultHandle of type boolean. Note that this code sequence fails at runtime with NullPointerException when obj1 represents the null reference.

Gizmo.systemOutPrintln(BytecodeCreator target, ResultHandle obj) generates an invocation of System.out.println(obj) into target. Note that this code sequence fails at runtime with ClassCastException when obj is not of type String.

Similarly, Gizmo.systemErrPrintln(BytecodeCreator target, ResultHandle obj) generates an invocation of System.err.println(obj) into target. Note that this code sequence fails at runtime with ClassCastException when obj is not of type String.

Gizmo.newArrayList(BytecodeCreator target) generates an invocation of new ArrayList() into target. There’s also a variant that takes a statically known initial capacity: Gizmo.newArrayList(BytecodeCreator target, int initialCapacity).

Gizmo.newHashSet(BytecodeCreator target) generates an invocation of new HashSet() into target.

Gizmo.newHashMap(BytecodeCreator target) generates an invocation of new HashMap() into target.

StringBuilders

Gizmo.newStringBuilder(BytecodeCreator target) generates an invocation of new StringBuilder() into target and returns a StringBuilderGenerator. The generator has an append(ResultHandle) method that generates an invocation of the correct overload of myStringBuilder.append(). There’s also a variant of append() that takes statically known char and String constants. After the string is built, StringBuilderGenerator.callToString() generates an invocation of myStringBuilder.toString() to finally build the String object.

Gizmo.newStringBuilder() Example
void buildString(BytecodeCreator bytecode) {
    // StringBuilder str = new StringBuilder();
    StringBuilderGenerator str = Gizmo.newStringBuilder(bytecode);
    // str.append(1);
    str.append(bytecode.load(1));
    // str.append('+');
    str.append(bytecode.load('+'));
    // str.append(1L);
    str.append(bytecode.load(1L));
    // str.append("=");
    str.append(bytecode.load("="));
    // str.append("???");
    str.append("???");
    // String result = str.toString();
    ResultHandle result = str.callToString();
    // System.out.println(result);
    Gizmo.systemOutPrintln(bytecode, result);
}

Operations Helpers

Several helper methods and classes are provided to generate method invocations on commonly used classes and their instances. They are all structured similarly. For example, when you call Gizmo.listOperations(BytecodeCreator), you get a JdkList. If you call JdkList.on(ResultHandle), where the parameter represents a java.util.List, you get a JdkListInstance. JdkList has methods to generate invocations of static methods from java.util.List, while JdkListInstance allows generating invocations of instance methods. Similar methods and classes exists for other types, such as Set, Map, Collection, or Optional.

Further, the classes such as JdkList are structured in an inheritance hierarchy that mirrors the actual inheritance hierarchy of List etc. So JdkList extends JdkCollection, which in turn extends JdkIterable:

          JdkIterable             JdkMap
               ^
               |
         JdkCollection            JdkOptional
           ^       ^
           |       |
        JdkList  JdkSet

Similarly, JdkListInstance extends JdkCollectionInstance, which in turn extends JdkIterableInstance:

      JdkIterableInstance         JdkMapInstance
               ^
               |
     JdkCollectionInstance        JdkOptionalInstance
         ^          ^
         |          |
JdkListInstance  JdkSetInstance

Therefore, if you have a JdkListInstance, you can generate a size() invocation, because JdkCollectionInstance has a method for it.

Iterable Operations

Gizmo.iterableOperations(BytecodeCreator target) returns JdkIterable with no additional methods.

JdkIterable.on(ResultHandle iterable) returns JdkIterableInstance with these methods:

  • iterator() to generate an invocation of myIterable.iterator()

Iterator Operations

Gizmo.iteratorOperations(BytecodeCreator target) returns JdkIterator with no additional methods.

JdkIterator.on(ResultHandle iterator) returns JdkIteratorInstance with these methods:

  • hasNext() to generate an invocation of myIterator.hasNext()

  • next() to generate an invocation of myIterator.next()

Gizmo.iterableOperations() and iteratorOperations() Example
void iterate(BytecodeCreator bytecode, ResultHandle iterable) {
    // Iterator iterator = iterable.iterator();
    ResultHandle iterator = Gizmo.iterableOperations(bytecode).on(iterable).iterator();

    // while (iterator.hasNext()) {
    //     Object next = iterator.next();
    //     System.out.println((String) next);
    // }
    WhileLoop loop = bytecode.whileLoop(bc -> bc.ifTrue(
          Gizmo.iteratorOperations(bc).on(iterator).hasNext()));
    BytecodeCreator block = loop.block();

    ResultHandle next = Gizmo.iteratorOperations(block).on(iterator).next();
    Gizmo.systemOutPrintln(block, next);
}

Collection Operations

Gizmo.collectionOperations(BytecodeCreator target) returns JdkCollection with no additional methods.

JdkCollection.on(ResultHandle colletion) returns JdkCollectionInstance with these methods:

  • size() to generate an invocation of myCollection.size()

  • isEmpty() to generate an invocation of myCollection.isEmpty()

  • contains(ResultHandle obj) to generate an invocation of myCollection.contains(Object)

  • add(ResultHandle element) to generate an invocation of myCollection.add(Object)

  • addAll(ResultHandle collection) to generate an invocation of myCollection.addAll(Collection)

  • clear() to generate an invocation of myCollection.clear()

Gizmo.collectionOperations() Example
void printSize(BytecodeCreator bytecode, ResultHandle collection) {
    JdkCollectionInstance collectionOps = Gizmo.collectionOperations(bytecode).on(collection);
    // int size = collection.size();
    ResultHandle size = collectionOps.size();
    // String sizeStr = "" + size;
    ResultHandle sizeStr = Gizmo.toString(bytecode, size); (1)
    // System.out.println(sizeStr);
    Gizmo.systemOutPrintln(bytecode, sizeStr);
}
  1. Here, we emit a toString() call on a primitive type (size is an int). Gizmo will insert an auto-boxing operation, so the toString() method is actually called on java.lang.Integer.

List Operations

Gizmo.listOperations(BytecodeCreator target) returns JdkList with these methods:

  • of() to generate an invocation of List.of()

  • of(ResultHandle e1) to generate an invocation of List.of(Object)

  • of(ResultHandle e1, ResultHandle e2) to generate an invocation of List.of(Object, Object)

  • of(ResultHandle e1, ResultHandle e2, ResultHandle e3) to generate an invocation of List.of(Object, Object, Object)

  • of(ResultHandle…​ elements) to generate an invocation of List.of(Object…​)

  • copyOf(ResultHandle collection) to generate an invocation of List.copyOf(Collection)

JdkList.on(ResultHandle list) returns JdkListInstance with these methods:

  • get(int index) to generate an invocation of myList.get(int)

  • get(ResultHandle index) to generate an invocation of myList.get(index).

Gizmo.listOperations() Example
void createListAndPrintFirst(BytecodeCreator bytecode) {
    JdkList listOps = Gizmo.listOperations(bytecode);
    // List list = List.of("element", "2nd element");
    ResultHandle list = listOps.of(bytecode.load("element"), bytecode.load("2nd element"));

    JdkListInstance listInstanceOps = listOps.on(list);
    // Object firstElement = list.get(0);
    ResultHandle firstElement = listInstanceOps.get(0);

    // System.out.println((String) firstElement);
    Gizmo.systemOutPrintln(bytecode, firstElement);
}

Set Operations

Gizmo.setOperations(BytecodeCreator target) returns JdkSet with these methods:

  • of() to generate an invocation of Set.of()

  • of(ResultHandle e1) to generate an invocation of Set.of(Object)

  • of(ResultHandle e1, ResultHandle e2) to generate an invocation of Set.of(Object, Object)

  • of(ResultHandle e1, ResultHandle e2, ResultHandle e3) to generate an invocation of Set.of(Object, Object, Object)

  • of(ResultHandle…​ elements) to generate an invocation of Set.of(Object…​)

  • copyOf(ResultHandle collection) to generate an invocation of Set.copyOf(Collection)

JdkSet.on(ResultHandle set) returns JdkSetInstance with no additional methods.

Gizmo.setOperations() Example
void createSetAndPrint(BytecodeCreator bytecode) {
    Gizmo.JdkSet setOps = Gizmo.setOperations(bytecode);
    // Set set = Set.of("element", "2nd element");
    ResultHandle set = setOps.of(bytecode.load("element"), bytecode.load("2nd element"));

    // String setStr = set.toString();
    ResultHandle setStr = Gizmo.toString(bytecode, set);
    // System.out.println(setStr);
    Gizmo.systemOutPrintln(bytecode, setStr);
}

Map Operations

Gizmo.mapOperations(BytecodeCreator target) returns JdkMap with these methods:

  • of() to generate an invocation of Map.of()

  • of(ResultHandle k1, ResultHandle v1) to generate an invocation of Map.of(Object, Object)

  • copyOf(ResultHandle map) to generate an invocation of Map.copyOf(Map)

JdkMap.on(ResultHandle map) returns JdkMapInstance with these methods:

  • get(ResultHandle key) to generate an invocation of myMap.get(Object)

  • put(ResultHandle key, ResultHandle val) to generate an invocation of myMap.put(Object, Object)

  • size() to generate an invocation of myMap.size()

  • isEmpty() to generate an invocation of myMap.isEmpty()

  • containsKey(ResultHandle key) to generate an invocation of myMap.containsKey(Object)

Gizmo.mapOperations() Example
void createMapAndLookup(BytecodeCreator bytecode) {
    JdkMap mapOps = Gizmo.mapOperations(bytecode);
    // Map map = Map.of("key", "value");
    ResultHandle map = mapOps.of(bytecode.load("key"), bytecode.load("value"));

    JdkMapInstance mapInstanceOps = mapOps.on(map);
    // Object value = map.get("key");
    ResultHandle value = mapInstanceOps.get(bytecode.load("key"));
    // System.out.println((String) value);
    Gizmo.systemOutPrintln(bytecode, value);
}

Optional Operations

Gizmo.optionalOperations(BytecodeCreator target) returns JdkOptional with these methods:

  • of(ResultHandle value) to generate an invocation of Optional.of(Object)

  • ofNullable(ResultHandle value) to generate an invocation of Optional.ofNullable(Object)

JdkOptional.on(ResultHandle optional) returns JdkOptionalInstance with these methods:

  • isPresent() to generate an invocation of myOptional.isPresent()

  • isEmpty() to generate an invocation of myOptional.isEmpty()

  • get() to generate an invocation of myOptional.get()

Gizmo.optionalOperations() Example
void createOptionalAndPrint(BytecodeCreator bytecode) {
    JdkOptional optionalOps = Gizmo.optionalOperations(bytecode);
    // Optional optional = Optional.of("value");
    ResultHandle optional = optionalOps.of(bytecode.load("value"));

    JdkOptionalInstance optionalInstanceOps = optionalOps.on(optional);
    // if (optional.isPresent()) {
    //     Object value = optional.get();
    //     System.out.println((String) value);
    // }
    BytecodeCreator ifPresent = bytecode.ifTrue(optionalInstanceOps.isPresent()).trueBranch();
    ResultHandle value = Gizmo.optionalOperations(ifPresent).on(optional).get();
    Gizmo.systemOutPrintln(ifPresent, value);
}

Generating equals, hashCode and toString

When creating a DTO-style class, it is often possible to generate the equals, hashCode and toString from a template. Similarly to IDEs generating their source code, Gizmo has utility methods to generate their bytecode.

To generate a structural equals method into given ClassCreator, based on given fields, use:

  • Gizmo.generateEquals(ClassCreator clazz, FieldDescriptor... fields)

  • Gizmo.generateEquals(ClassCreator clazz, Collection<FieldDescriptor> fields)

To generate a structural equals and hashCode methods into given ClassCreator, based on given fields, use:

  • Gizmo.generateEqualsAndHashCode(ClassCreator clazz, FieldDescriptor... fields)

  • Gizmo.generateEqualsAndHashCode(ClassCreator clazz, Collection<FieldDescriptor> fields)

Finally, to generate a naive toString method into given ClassCreator, based on given fields, use:

  • Gizmo.generateNaiveToString(ClassCreator clazz, FieldDescriptor... fields)

  • Gizmo.generateNaiveToString(ClassCreator clazz, Collection<FieldDescriptor> fields)

These methods require explicitly passing the set of fields to consider. When you know that all fields must be considered, it is easy to express that.

Gizmo.generateEqualsAndHashCode() and generateNaiveToString() Example
void createDTO(ClassOutput output) {
    try (ClassCreator creator = ClassCreator.builder()
            .classOutput(output)
            .className("com.example.Person")
            .build()) {

        creator.getFieldCreator("name", String.class).setModifiers(Opcodes.ACC_FINAL);
        creator.getFieldCreator("age", int.class).setModifiers(Opcodes.ACC_FINAL);

        // generate constructor here

        Gizmo.generateEqualsAndHashCode(creator, creator.getExistingFields());
        Gizmo.generateNaiveToString(creator, creator.getExistingFields());
    }
}

Transforming an Existing Class

In addition to creating classes, Gizmo also provides a limited form of class transformation. In order to transform the bytecode of an existing class, we need to create a ClassTransformer instance, configure the class transformation, and then apply it to a ClassVisitor. The result is another ClassVisitor that should be used instead of the original.

ClassTransformer transformer = new ClassTransformer(className); (1)
// ...do some transformations
ClassVisitor visitor = transformer.applyTo(originalVisitor); (2)
  1. ClassTransformer needs to know the name of class that is being transformed.

  2. ClassTransformer#applyTo() takes a ClassVisitor and returns another ClassVisitor that performs the transformation. The ClassVisitor passed to applyTo must not have been visited yet.

Adding Fields

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

// public final String bar;
FieldCreator fc = ct.addField("bar", String.class); (1)
fc.setModifiers(Opcodes.ACC_PUBLIC | OpCodes.ACC_FINAL);

ClassVisitor visitor = ct.applyTo(...);
  1. Use the FieldCreator API to configure the new field.

Removing Fields

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

// public final String bar;
ct.removeField("bar", String.class); (1)

ClassVisitor visitor = ct.applyTo(...);
  1. Removes the field with name bar and type java.lang.String.

Adding Methods

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

// public final String transform(String val) {
//    return val.toUpperCase();
// }
MethodCreator transform = ct.addMethod("transform", String.class, String.class); (1)
ResultHandle ret = transform.invokeVirtualMethod(
        MethodDescriptor.ofMethod(String.class, "toUpperCase", String.class),
        transform.getMethodParam(0));
transform.returnValue(ret);

ClassVisitor visitor = ct.applyTo(...);
  1. Use the MethodCreator API to configure the new method.

Removing Methods

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

// public final String transform(String val) {
//    return val.toUpperCase();
// }
ct.removeMethod("transform", String.class, String.class); (1)

ClassVisitor visitor = ct.applyTo(...);
  1. Removes the method with name transform, return type java.lang.String and parameter java.lang.String.

Adding and Removing Modifiers

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

ct.removeModifiers(OpCodes.ACC_FINAL); (1)

ClassVisitor visitor = ct.applyTo(...);
  1. Use removeModifiers to remove modifiers from the class. The complementary method is called addModifiers.

Implementing New Interfaces

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

ct.addInterface(Function.class); (1)

MethodCreator method = ct.addMethod("apply", Object.class, Object.class); (2)
method.returnValue(...);

ClassVisitor visitor = ct.applyTo(...);
  1. Call addInterface to add an interface to the list of interfaces of the class.

  2. Use addMethod to implement all the methods prescribed by the interface.

Modifying Methods and Fields

The methods modifyMethod return a MethodTransformer, which is used to configure transformations on a given method. Similarly, the modifyField methods return FieldTransformer.

Renaming

Renaming a method and then adding a new method with the old name is an easy way to "intercept" the previous method. Say the class org.acme.Foo has the following method:

public String transform(int value) {
    return "result: " + value;
}

Then, the following transformation:

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

ct.modifyMethod("transform", String.class, int.class).rename("transform$"); (1)

MethodCreator transform = ct.addMethod("transform", String.class, int.class); (2)
ResultHandle originalResult = transform.invokeVirtualMethod(
        MethodDescriptor.ofMethod("org.acme.Foo", "transform$", String.class, int.class),
        transform.getThis(), transform.getMethodParam(0));
ResultHandle result = Gizmo.newStringBuilder(transform)
        .append("intercepted: ")
        .append(originalResult)
        .callToString();
transform.returnValue(result);

ClassVisitor visitor = ct.applyTo(...);
  1. Rename the transform method to transform$.

  2. Add a new transform method that delegates to transform$.

modifies the class to look like this:

// previous method, renamed but otherwise kept intact
public String transform$(int value) {
    return "result: " + value;
}

// new method, delegates to the renamed old method (but does not necessarily have to)
public String transform(int value) {
    return "intercepted: " + transform$(value);
}

Fields may be renamed in a similar fashion.

Adding and Removing Modifiers

ClassTransformer ct = new ClassTransformer("org.acme.Foo");

ct.modifyField("val", String.class).removeModifiers(Modifier.FINAL); (1)

ClassVisitor visitor = ct.applyTo(...);
  1. Use removeModifiers to remove modifiers from given member. In this case, it’s the field val of type String. The complementary method is, again, called addModifiers.