How to extend GluonJ
Shigeru Chiba and Muga Nishizawa
It is easy to extend GluonJ so that it will provide a new pointcut
designator. An extension is written in GluonJ itself. In fact,
the implementation of the annotate
pointcut of GluonJ
is a @Glue
class in the javassist.gluonj.plugin
package. This document introduces this implementation to show how GluonJ
can be extended.
The architecture of GluonJ
A @Glue
class is represented by a Gluon
object inside of GluonJ. A Gluon
object holds a list of
@Refine
classes and pairs of pointcut and advice.
If GluonJ reads a @Glue
class, it first makes an instance
of the @Glue
class. The initial values of fields of type
Pointcut
are checked and the abstract syntax trees for the
pointcuts are generated. The node type of the abstract syntax tree is
javassist.gluonj.pc.PointcutNode
. A Gluon
object holds this abstract syntax tree as a pointcut.
While weaving, GluonJ reads method bodies of a transformed class.
Whenever it encounters a join point such as a method call and
a field access, it examines whether or not the join point matches
one of the pointcuts in a Gluon
object.
This examination is performed according to the visitor pattern.
If the join point matches a pointcut, then GluonJ inserts an advice
body at that join point.
Therefore, a new pointcut designator can be added to GluonJ by the following steps:
Declare a tree-node class, which is a subclass of
PointcutNode
. It is used for representing the new pointcut designator.Refine all visitor classes used in GluonJ. They are subclasses of
javassist.gluonj.pc.PointcutVisitor
.Refine the
Pcd
andPointcut
classes.Finally, install a
@Glue
implementing the new pointcut designator. It is done by modifyingjavassist.gluonj.plugin.Installed
.
Implement the annotate
pointcut designator
The implementation of the annotate
pointcut designator
is in javassit.gluonj.plugin.MetaTag
, which is a single
@Glue
class. We blow show the source code of this
@Glue
class so that the readers can understand how
a @Glue
class is used for extending GluonJ.
The annotate
pointcut designator is used by the GluonJ users
for selecting join points at which accessed methods or fields are
annotated by a specific annotation. For example,
@Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("*(..)").and.annotate("@test.Print");
This pointcut selects all calls to the methods annotated by
@test.Print
.
Turning debug traces on/off
The MetaTag
class implementing the annotate
pointcut designator starts with the following static nested class:
package javassist.gluonj.plugin; import javassist.gluonj.*; // and more... @Glue public class MetaTag { @Refine("javassist.gluonj.Gluon") public static class Debug { public static boolean stackTrace = true; } // more nested classes follow...
If the value of stackTrace
field declared in
the Gluon
class is true, GluonJ prints a stack trace
when it prints an error message. The value of stackTrace
is normally false but you might want to turn it true when you are
debugging a new pointcut designator. The @Refine
class
above is for turning the value of stackTrace
true.
If you want to turn it false, you must
comment out @Refine("javassist.gluonj.Gluon")
.
A class not annotated by @Refine
is just ignored by GluonJ.
Declare Tree-node classes
First, we declare an interface for calling a visitor method that we
will add for the annotate
pointcut designator:
public interface AnnotatePcVisitor { void visit(AnnotatePc pc) throws WeaveException; }
This is a nested interface declared in the MetaTag
class
(a @Glue
class).
Then, we declare a class representing a tree node for the
annotate
pointcut designator:
public static class AnnotatePc extends PointcutNode { private String arg; private PcPattern pattern; public AnnotatePc(String pat) { this.arg = pat; } public String toString() { return "annotate(" + arg + ")"; } public void prepare(Parser p) throws WeaveException { pattern = p.parseClass(removeAt(arg)); if (!pattern.isClassName()) throw new WeaveException("bad pattern: " + toString()); } private static String removeAt(String name) { if (name != null && name.length() > 1 && name.charAt(0) == '@') return name.substring(1); else return name; } public void accept(PointcutVisitor v) throws WeaveException { ((AnnotatePcVisitor)v).visit(this); } /* called by a visitor for determining a given join point matches * a pattern. */ public boolean match(AnnotationsAttribute attr) throws WeaveException { if (attr == null) return false; Annotation[] anno = attr.getAnnotations(); int n = anno.length; for (int i = 0; i < n; i++) if (pattern.matchClass(anno[i].getTypeName())) return true; return false; } }
This class must have the accept
method for running a visitor.
It calls the visit
method in the AnnotatePcVisitor
interface declared in this @Glue
class. We cannot directly
call the visit
method since it has not been declared in
the PointcutVisitor
interface. It will be added below.
The prepare
and match
methods are used
for implementing the annotate
pointcut.
The prepare
method is called once when GluonJ starts weaving.
It parses a given pattern stored in arg
so that the
match
method will not have to parse the pattern every time.
The match
method returns true if a given annotation matches
the pattern. It is called by a visitor.
Refine visitor classes
Since we have defined a new node AnnotatePc
, we also have
to modify the PointcutVisitor
interface so that the new node
will be visited.
We modify the PointcutVisitor
interface to inherit
from the AnnotatePcVisitor
that we defined above.
It declares the visit
method taking an AnnotatePc
object as an argument.
@Refine("javassist.gluonj.pc.PointcutVisitor") interface PcVisitor extends AnnotatePcVisitor {}
Then, we modify the visitor classes implementing the
PointcutVisitor
interface.
@Refine("javassist.gluonj.Pointcut.PrepareVisitor") public static class Prepare { Parser parser; public void visit(AnnotatePc pc) throws WeaveException { pc.prepare(parser); } }
The prepare
method in the PrepareVisitor
class just calls the prepare
method
in the AnnotatePc
class.
@Refine("javassist.gluonj.weave.Matcher") public static class Match { protected boolean result; protected Residue residue; public void visit(AnnotatePc pc) throws WeaveException { result = false; } }
The Matcher
class is an abstract super class of
the visitor classes used for determining whether or not a given
join point matches a pointcut. While weaving, GluonJ reads every
method body and, when it encounters a join point such as a method
call, it runs a visitor for finding a pointcut-advice pair that
matches the join point. The visitor traverses the abstract syntax
tree of every registered pointcut and determines whether or not the
pointcut matches the join point. If it does, GluonJ modifies bytecode
so that the paired advice will be executed at that join point.
The visit
method determines whether or not
the tree node given as an argument (or the sub-tree whose root is
the given node) matches the join point. If the tree node matches,
the result
field is set to true
. Otherwise,
it is set to false
. If a residue remains, a Java expression
representing that residue is set to the residue
field.
Both these fields are in the Matcher
class.
The residue is a runtime condition for executing an advice.
If residule
is non-null, GluonJ modifies bytecode so that
an advice body will be executed only when the runtime value of
the Java expression stored in residue
is true.
The modified bytecode will be something like this:
if (a Java expression stored inresidue
) execute an advice body ;
GluonJ uses a different visitor for each kind of join point.
For the annotate
pointcut,
we modify two subclasses of Matcher
: one is for method calls
and the other is for field accesses. We define a visit
method
for each:
@Refine("javassist.gluonj.weave.CallMatcher") public static class CallMatch { private MethodCall joinPoint; protected boolean result; public void visit(AnnotatePc pc) throws WeaveException { try { MethodInfo minfo = joinPoint.getMethod().getMethodInfo2(); AnnotationsAttribute aa1 = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.invisibleTag); AnnotationsAttribute aa2 = (AnnotationsAttribute) minfo.getAttribute(AnnotationsAttribute.visibleTag); result = pc.match(aa1) || pc.match(aa2); } catch (NotFoundException e) { throw new WeaveException(e); } } } @Refine("javassist.gluonj.weave.FieldMatcher") public static class FieldMatch { boolean result; private FieldAccess joinPoint; public void visit(AnnotatePc pc) throws WeaveException { try { FieldInfo finfo = joinPoint.getField().getFieldInfo2(); AnnotationsAttribute aa1 = (AnnotationsAttribute) finfo.getAttribute(AnnotationsAttribute.invisibleTag); AnnotationsAttribute aa2 = (AnnotationsAttribute) finfo.getAttribute(AnnotationsAttribute.visibleTag); result = pc.match(aa1) || pc.match(aa2); } catch (NotFoundException e) { throw new WeaveException(e); } } }
The CallMatch
and FieldMatch
classes
declare a field named joinPoint
. The value of that field
represents a join point currently examined.
The last visitor is the CflowCollector
.
It is used for collecting methods that GluonJ must monitor for
the cflow
pointcut.
GluonJ modifies bytecode so that the entries
and exits of a thread into/from the collected methods will be
recorded during runtime. Since the annotate
pointcut
has nothing to do with the cflow
pointcut,
the visit
method that we add to the CflowCollector
does nothing.
@Refine("javassist.gluonj.weave.CflowCollector") public static class Cflow { public void visit(AnnotatePc pc) throws WeaveException { // do nothing. } }
Refine Pcd
and Pointcut
Now, we refine the Pcd
and Pointcut
classes
so that the annotate
method will be available.
The annotate
method is used by the GluonJ users
for defining a pointcut.
The following declaration is an example:
@Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("*(..)").and.annotate("@test.Print");
We below show a @Refine
class that adds the
annotate
method to the Pointcut
class:
public interface AnnotateAvailable { Pointcut annotate(String tag); } @Refine("javassist.gluonj.Pointcut") static abstract class Pcut implements AnnotateAvailable { abstract void setTree(PointcutNode pcn); public Pointcut annotate(String tag) { setTree(new AnnotatePc(tag)); return (Pointcut)Gluon.$refine(this); } }
The AnnotateAvailable
interface is for accessing
the annotate
method that we add to the Pointcut
class.
The annotate
method first makes an instance of
the AnnotatePc
class
we have defined above. Then
it records that instance as a leaf-node of this Pointcut
object. setTree
is a method declared in the original
Pointcut
.
The last statement of the annotate
method:
return (Pointcut)Gluon.$refine(this);
returns a reference to this Pointcut
object.
Gluon.$refine
is a special method for type conversion
from Pcut
to Pointcut
. Note that
this
in the annotate
method declared
in the Pcut
class refers to a Pcut
object.
Although the Pcut
class will be identical to the
Pointcut
class after weaving, it is not yet when the
Pcut
class is compiled. Therefore, we must explicitly
perform type conversion by using the following coding idiom:
( type-name )Gluon.$refine( value )
We must also define the annotate
method in the
Pcd
class.
@Refine("javassist.gluonj.Pcd") static class Pcd2 { @Abstract private static Pointcut make() { return null; } public static Pointcut annotate(String tag) { AnnotateAvailable pc = (AnnotateAvailable)make(); return pc.annotate(tag); } }
Since a constructor of Pointcut
is not public
,
we instead use the make
method in the Pcd
class,
which is a static
method for creating a Pointcut
object. The type of variable pc
must
be AnnotateAvailable
shown above.
Otherwise, we could not call the annotate
method that we added
to the Pointcut
class.
To call the make
method within the Pcd
class (the @Refine
class) shown above,
we must contain the following line in the declaration of
the Pcd
class:
@Abstract private static Pointcut make() { return null; }
This tells GluonJ that a call to the make
method
within the Pcd
class is interpreted as a call to
the make
method declared in the Pointcut
class.
Install everything
The final step is to install this @Glue
class we have
shown above. We must contain the following line in
the javassist.gluonj.plugin.Installed
class:
@Include MetaTag glue0;
The Installed
class is also a @Glue
class.
The build script of GluonJ weaves it at the last step of building GluonJ.
The resulting declaration of the Installed
class is the
following:
@Glue public class Installed { @Include MetaTag glue0; }
If a @Glue
class contains a field annotated
by @Include
, the type of that field must be also
woven. It must be a @Glue
class.
The field name glue0
does not have any special meaning.
You can choose another name.
To rebuild GluonJ according to the modified Installed
class, you must run the ant
command twice:
ant ant -buildfile build-plugin.xml
The first run uses build.xml
for a build file
and produces
gluonj.jar
without any plug-ins. Then the second run
weaves plug-ins listed in the Installed
class. It
produces a complete version of gluonj.jar
.
Copyright (C) 2006 by Shigeru Chiba and Muga Nishizawa. All rights reserved.