Introduction to GluonJ

Shigeru Chiba

Table of Contents

1. A reviser
2. More details of reviser
3. Within methods
4. Limitations
5. Using a reviser in Java


1. A reviser

GluonJ is an extension to Java. It introduces a new class-like construct named a reviser. A reviser extends the definition of an existing class like the intertype declaration of AspectJ. Although the declaration of a reviser is similar to that of a subclass, a reviser directly modifies the definition of an existing class; it can add a new method and override a method in the target class. A subclass, on the other hand, defines a new class with extension while the original class (i.e. the super class) remains.

Suppose that the following class has been declared:

package test;
public class Person {
    public void greet() {
        System.out.println("Hi!");
    }
    public static void main(String[] args) {
        new Person().greet();
    }
}

Let us extend this class definition for test.Person:

package sample;
public reviser SayHello extends test.Person {
    public void greet() {
        System.out.println("Hello!");
    }
}

The SayHello is a reviser. It revises directly its target class test.Person. Thus, the implementation of the greet method in the Person class is replaced with one in the SayHello reviser.

To compile the program above, you have to do the following:

java -jar GluonJCompiler.jar test/Person.java sample/SayHello.java
java -jar gluonj.jar test/Person.class sample/SayHello.class

We assume that GluonJCompiler.jar and gluonj.jar are in the current directory.

The compilation of a GluonJ program consists of two phases: source-to-bytecode translation and bytecode-to-bytecode translation. We call them compilation and post-compile transformation. The first command using GluonJCompiler.jar performs the first phase. It takes options almost compatible to javac. The second command using gluonj.jar performs the second phase. It takes three options, which are -debug (set debug mode to on), -d (output directory), and -cp (class path). Note that the second command takes .class (not .java!) files as arguments.

Then, to run the program, do the following:

java test.Person

The program will print Hello!. Note that the main method creates a Person object and calls greet on that object. Since SayHello is a reviser, its greet substitutes for the original greet method in Person. If SayHello were a subclass, the program would print Hi! since the main method does not create a SayHello object but a Person object, whose greet method prints Hi!. This resulting behavior is similar to what dependency injection frameworks provide.

Ant task

The compilation of a GluonJ program can be performed as an ant task. Currently, only the second phase (post-compile transformation) can be an ant task. The following is an example of build.xml file:

<?xml version="1.0"?>
<project name="hello" basedir=".">
    <taskdef name="weave" classname="javassist.gluonj.ant.taskdefs.Weave">
        <classpath>
            <pathelement location="./gluonj.jar"/>
        </classpath>
    </taskdef>

    <target name="weave">
        <weave destdir="./out" debug="false" >
            <classpath>
                <pathelement path="./classes"/>
            </classpath>
            <fileset dir="./classes" includes="test/**/*" />
        </weave>
    </target>
</project>

This build.xml file assumes that gluonj.jar is in the current directory. It also assumes that the class files produced by the first command are in the ./classes directory and they are saved in the ./out directory after the transformation. The task processes class files specified by the fileset element. In the example above, all class files in the ./classes/test (classes whose package names start with test) are processed.

The weave task has the debug attribute. If it is true, GluonJ will print detailed log messages. This attribute is optional. The default value is false.

Load-time weaving

The post-compile transformation can be postponed till load time. If you want to do that, the -javaagent command-line option must be given to the JVM. For example,

java -javaagent:gluonj.jar=sample.SayHello test.Person

Again, we assume that gluonj.jar is in the current directory. The class name following gluonj.jar= is a reviser's name. If there are multiple revisers, reviser names separated by commas must be listed here. (As we show later, other revisers required by a reviser listed here are automatically included in the list of effective revisers.)

Note that -javaagent:... is a VM argument. This might be important when you run the program on Eclipse or other IDEs.

If you want to see detailed log messages, you must specify the debug option. For example,

java -javaagent:gluonj.jar=debug:sample.SayHello test.Person

Note that debug: is inserted after =.

Load-time weaving without a Java agent

You can perform load-time weaving without the -javaagent option. Write the following launcher class:

import javassist.gluonj.util.Loader;

public class Runner {
    public static void main(String[] args) throws Throwable {
        Class mainClass = test.Person.class;
        Class[] revisers = { sample.SayHello.class };
        Loader.run(mainClass, args, revisers);
    }
}

The main method in Runner runs the main method in Person with command-line arguments args after weaving the SayHello reviser.

The following commands compile and run the launcher class:

javac -cp .:gluonj.jar Runner.java 
java -cp gluonj.jar Runner

Version number

If no arguments are given, gluonj.jar prints the version number and the copyright notices of GluonJ.

java -jar gluonj.jar

2. More details of revisers

A reviser is similar to a subclass. The declaration syntax of revisers is the same as that of the subclasses except the reviser keyword. On the other hand, a reviser cannot be instantiated. If a reviser declares a new method, it appends that method to its target class. If a reviser overrides a method in the target class, the method implementation in the reviser is substituted for the original implementation in the target class. The original implementation is executed only when the reviser's method calls on super. Furthermore, if a reviser implements an interface, it appends that interface to the list of the implemented interfaces of the target class.

A reviser follows the visibility rule of Java. A reviser can only access visible members of its target class. For example, it cannot access private members of its target class. It cannot override a final method of its target class.

The semantics of revisers might be easily understood by seeing the current implementation of GluonJ. Roughly speaking, a GluonJ compiler translates a reviser into a normal subclass in Java. Its super class is the target class. Then all occurrences of instantiation of the target class are also translated into instantiation of that subclass. This translation implements the basic behavior of a reviser.

Override static methods

Unlike a normal class, a reviser can override a static method in its target class. For example,

package test;
public class Fact {
    public static int fact(int n) {
        if (n == 1)
            return 1;
        else
            return n * fact(n - 1);
    }
    public static void main(String[] args) {
        System.out.println(fact(5));
    }
}
package sample;
import test.Fact;
public reviser FactLogger extends Fact {
    public static int fact(int n) {
        System.out.println("* " + n);
        return Fact.fact(n);
    }
}

The FactLogger reviser overrides the static method fact originally declared in the Fact class. A call from the main method in Fact executes the implementation of fact declared in FactLogger. Only a call from within FactLogger executes the original implementation in Fact.

Requires

GluonJ allows multiple revisers to have the same target class. In that case, programmers have to specify the precedence order among those revisers.
package sample;
public reviser GoodDay requires SayHello extends test.Person {
    public void greet() {
        super.greet();
        System.out.println("It's a good day today.");
    }
}

The requires clause between the reviser name and the extends clause specifies another reviser that this reviser requires. If a reviser R requires another reviser S, then the required reviser S is first applied to their target class before the requiring reviser R is applied. For example, since GoodDay requires SayHello, the SayHello reviser is applied to the test.Person class before GoodDay. Thus, when the greet method is called on a Person object, the implementation in GoodDay is first executed. When the greet method in GoodDay calls on super, the greet method in SayHello is next executed. This behavior is Last In, First Out (LIFO). A method in a reviser applied the last is executed first.

Multiple reviser names separated by commas can follow requires. If a reviser R requires multiple revisers S1, S2, S3, ..., then the revisers S1, S2, S3, ... are applied to the target class in this ordering. The requiring reviser R is applied last. A call on super by a reviser R executes the implementation in the reviser applied to the target class just before that calling reviser R.

The relation among revisers specified by the requires clauses must be total order. Otherwise, the post-compile transformation reports an error.

This might look annoying but it simplifies load-time weaving. For example, since GoodDay requires SayHello, you can run test.Person by the following command:

java -javaagent:gluonj.jar=sample.GoodDay test.Person

Since GluonJ automatically loads all required revisers, not only sample.GoodDay but also sample.SayHello is included in the list of the revisers. You do not have to list all revisers like this:

java -javaagent:gluonj.jar=sample.GoodDay,sample.SayHello test.Person

A reviser can require another reviser whose target class is different from the target of the requiring reviser. Thus, you can write a reviser that directly or indirectly requires all the revisers necessary in your program. Then you can run the program by specifying only that reviser as an argument to the load-time weaver.

Using

A reviser R can access members added by the revisers that the reviser R requires if those members are public or visible from that reviser R. For example,

package test;
public class LinkNode {
    protected LinkNode next = null;
    public static void main(String[] args) {
        LinkNode n = new LinkNode();
        System.out.println(n);
    }
}
package sample;
import test.LinkNode;
public reviser IntNode extends LinkNode {
    public int value;
    public int get() { return value; }
}
package sample;
import test.LinkNode;
public reviser PrintableNode requires IntNode  extends LinkNode {
    public String toString() { return "Node:" + get() + " " + value; }
}

Since PrintableNode requires IntNode, the toString method in PrintableNode accesses the get method and the value field added by IntNode. Note that these members are not declared in either PritableNode or LinkNode.

A reviser can access members added by another reviser if the former requires the latter. On the other hand, how does a normal class access them? A normal class cannot require a necessary reviser by using a requires clause.

If a normal class needs to access members added by a reviser, it uses the using declaration.

package test;
using sample.IntNode;
public class NodeTest {
    public static void main(String[] args) {
        LinkNode n = new LinkNode();
        System.out.println(n.value);
    }
}

The using declaration is similar to the import declaration. It declares that the members added by a reviser will be used in the source file. All classes in that source file can access those members.

3. Within methods

A reviser can declare a within method. Unlike a normal method, the implementation of a within method is effective only when it is called from a specific call site. A within method usually override a method in its target class. The overriding is effective only when the method is called from the specific call site. A within method can be used like the within pointcut of AspectJ.

For example,

package test;
public class Position {
    public int x;
    public void rmove(int dx) { setX(x + dx); }
    public void setX(int newX) {
        System.out.println("setX");
        x = newX;
    }
}
package test;
public class PosTest {
    public static void main(String[] args) {
        Position p = new Position();
        p.setX(5);
        p.rmove(11);
    }
}

Let us change this program so that a log message will be printed when the main method calls setX but not when the rmove method calls setX. One approach is to modify the method body of main but we do not have to modify it by hand if we use a within method.

package sample;
public reviser PosLogger extends test.Position {
    public void setX(int newX) within test.PosTest.main(String[]) {
        System.out.println("x: " + x + ", newX: " + newX);
        super.setX(newX);
    }
}

Note that the setX method has a within predicate. This specifies this implementation of the setX method is effective only when the method is called from the main method in the PosTest class.

If you run the program above, the result will be:

x: 0, newX: 5
setX
setX

When the rmove method in Position calls setX, the implementation in the PosLogger reviser will not be executed. The original implementation in Position will be.

A within predicate can only specify a class name. In that case, the implementation of that within method is effective only when the method is called from a method directly declared in that class. The following is an example:

package sample;
public reviser PosLogger extends test.Position {
    public void setX(int newX) within test.PosTest {
        System.out.println("x: " + x + ", newX: " + newX);
        super.setX(newX);
    }
}

The implementation of setX in PosLogger is effective only when setX is called from any method declared in the PosTest class.

4. Limitations

GluonJ currently has several limitations.

  • A reviser cannot override or add a new constructor.

    A reviser cannot declare any constructors. When you want to add a new constructor, you must instead add a factory method by a reviser.

  • A reviser cannot declare multiple within methods with the same signature.

    They must be added by multiple revisers targeting the same class.

5. Using a reviser in Java

If you use Java annotations to describe GluonJ's revisers, you can use revisers in your plain Java programs. You can compile revisers by a normal Java compiler and run them with only the post-compile transformation. In fact, the first-phase transformation of GluonJ translates a GluonJ program into Java bytecode including annotations mentioned in this section.

Let us rewrite the SayHello reviser shown above so that it will use an annotation:

package sample;
import javassist.gluonj.Reviser;

@Reviser public class SayHello extends test.Person {
    public void greet() {
        System.out.println("Hello!");
    }
}

Now the SayHello is a plain class. It does not revise but extends the Person class. On the other hand, the SayHello class has an annotation @Reviser, which represents this class is a reviser.

To run the program, you execute the following command:

javac -cp .:gluonj.jar test/Person.java sample/SayHello.java
java -jar gluonj.jar test/Person.class sample/SayHello.class
java test.Person

You can also choose load-time weaving:

javac -cp .:gluonj.jar test/Person.java sample/SayHello.java
java -javaagent:gluonj.jar=sample.SayHello test.Person

In either case, the SayHello class revises the definition of the Person class.

The requires clauses is also replaced with an annotation. For example, the GoodDay reviser shown above can be rewritten as following:

package sample;
import javassist.gluonj.Reviser;
import javassist.gluonj.Require;

@Reviser @Require(SayHello.class)
public class GoodDay extends test.Person {
    public void greet() {
        super.greet();
        System.out.println("It's a good day today.");
    }
}

The @Require annotation declares that the GoodDay reviser requires the SayHello reviser. The argument to @Require is an array of java.lang.Class. Thus @Require can declare that multiple revisers are required. For example, if SayChao is a reviser, the following annotation is valid:

@Require({SayHello.class, SayChao.class})

Multiple revisers are surrounded with curly brackets. This annotation declares that SayHello and SayChao are required in this order.

A within method is indicated by @Within and @Code annotations. For example,

package sample;
import javassist.gluonj.Reviser;
import javassist.gluonj.Within;
import javassist.gluonj.Code;

@Reviser public class PosLogger extends test.Position {
    @Within(test.PosTest.class) @Code("main(java.lang.String[])")
    public void setX(int newX) {
        System.out.println("x: " + x + ", newX: " + newX);
        super.setX(newX);
    }
}

In this reviser, setX is a within method. It is effective only when it is called from the main method in test.PosTest. The argument to @Within is a java.lang.Class object. The @Code annotation specifies a method signature. It is optional. If only @Within is given, the within method is effective when it is called from within the class specified by that @Within annotation. Note that a fully qualified class name must be used in the character string given to @Code as a method signature.

Grouping

Multiple revisers related to the same concern should be in the same source file. To do so, a static nested class can be an @Reviser class. Related @Reviser classes can be grouped as static nested classes in one @Reviser class. Those @Reviser classes do not have to target the same class. For example,

package sample;
import javassist.gluonj.Reviser;

@Reviser public class Say {
    @Reviser public static class SayHello extends test.Person {
        public void greet() {
            System.out.println("Hello!");
        }
    }

    @Reviser public static class GoodDay extends test.Person {
        public void greet() {
            super.greet();
            System.out.println("It's a good day today.");
        }
    }
}

An @Reviser class named Say includes two static nested classes annotated with @Reviser. These static nested classes are implicitly required by the @Reviser class Say in the alphabetical order (the order may depend on a compiler). Note that the super class of Say is not explicitly specified (hence it is java.lang.Object). Such an @Reviser class does not revise any class. It simply requires static nested classes within its body if they are annotated with @Reviser.

The two nested classes SayHello and GoodDay must be static. Inner classes cannot be @Reviser classes.

To run the program above, do:

javac -cp .:gluonj.jar test/Person.java sample/Say.java
java -javaagent:gluonj.jar=sample.Say test.Person

In the second command, sample.Say is given as an argument. This automatically weaves the nested @Require classes as well.

Accessing new members added by revisers

A drawback of using Java annotations is that accessing new members added by revisers is verbose. Since a Java compiler does not understand the semantics of revisers, when you access a new member added by a reviser, you have to explicitly convert the type of a target object into the reviser type. You have to cheat a Java compiler.

For example,

package sample;
import test.LinkNode;
import javassist.gluonj.Reviser;
import javassist.gluonj.Require;
import static javassist.gluonj.GluonJ.revise;

@Reviser @Require(IntNode.class)
public class PrintableNode extends LinkNode {
    public String toString() {
        IntNode in = (IntNode)revise(this);
        return "Node:" + in.get() + " " + in.value;
    }
}

The PrintableNode reviser requires sample.IntNode and revises test.LinkNode. Both sample.IntNode and test.LinkNode are the same as ones already shown above.

To access new members get and value added by the IntNode reviser, the toString method converts the type of this from PrintableNode to IntNode. Otherwise, those members would be invisible from toString. The revise method is a static method in GluonJ. It is a simple identity function; receiving an argument of the Object type and returning it as is. Calling the revise method is equivalent to the following code:

public String toString() {
    IntNode in = (IntNode)(Object)this;
    return "Node:" + in.get() + " " + in.value;
}

The Java compiler of JDK 1.6 accepts this type conversion as well.

Note that @Require annotations are still necessary to specify the precedence order among revisers. If the order between IntNode and PrintableNode is not given, an error will be reported. There is no Java annotation representing using declarations since they only control the visibility of members added by revisers.

Constructors of an @Reviser class

Since a reviser is not a normal class, the definitions of @Reviser classes, which are classes annotated with @Reviser, must follow the following restrictions:

  • No subclass of an @Reviser class can be defined.

  • No instance of an @Reviser class can be made.

  • An @Reviser class must declare the same set of constructors as its target class. For every constructor in its target class, an @Reviser class must declare a constructor with the same signature.

For example,

package test;
import sample.Incrementable;
import static javassist.gluonj.GluonJ.revise;

public class Counter {
    protected int counter;
    public Counter(int c) { counter = c; }
    public int get() { return counter; }
    public void decrement() {
        if (--counter <= 0)
            throw new RuntimeException("Bang!");
    }
    public static void main(String[] args) {
        Counter c = new Counter(1);
        ((Incrementable)revise(c)).increment();
        c.decrement();
        System.out.println(c.get());
    }
}

Since this Counter class declares only one constructor, an @Reviser class revising this class declares one constructor taking a single integer parameter:

package sample;
import javassist.gluonj.Reviser;

@Reviser public class Incrementable extends test.Counter {
    private int delta;
    public Incrementable(int c) {
        super(c);
        delta = 1;
    }
    public void increment() {
        counter += delta;
    }
}

If the Counter class declares more than one constructor, Incrementable must declare the same number of constructors. Note that an @Reviser class cannot add a new constructor unless the target class does declare a constructor with the same signature as that new constructor.

For convenience, if a target class declares a number of constructors, an @Reviser class can declare only a default-constructor, which is a constructor taking no parameter. If a target class does not have the default constructor that takes no parameter, the default-constructor of an @Reviser class must call a non-default constructor of the target class. However, this call is ignored when the @Reviser class is woven.

package sample;
import javassist.gluonj.Reviser;

@Reviser public class Incrementable extends test.Counter {
    private int delta;
    public Incrementable() {
        super(0);
        delta = 1;
    }
    public void increment() {
        counter += delta;
    }
}

when a reviser Incrementable is woven, a copy of the body of the constructor of Incrementable is appended to the constructor of Counter. However, the call to the constructor of the super class is removed from the copy. For example, super(0) in the constructor of Incrementable is not copied. The constructor of Counter after weaving Incrementable is equivalent to the following:

public Counter(int c) {
    counter = c;
    delta = 1;          // copied from the @Reviser class
}

Copyright (C) 2006-2010 by Shigeru Chiba. All rights reserved.