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.