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 static class Debug extends Gluon { public Debug() { super(null); } 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
.
A class not annotated by @Refine
is just ignored by GluonJ.
Declare Tree-node classes
First, 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 { ((PcVisitor2)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 PcVisitor2
interface declared in this @Glue
class. We cannot directly
call the visit
method since it has not been declared in
the PointcutVisitor
interface. The PcVisitor2
interface will be defined 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.
First, we extend the PointcutVisitor
interface to have
a new visit
method.
The new visit
method takes an AnnotatePc
object as an argument.
@Refine interface PcVisitor2 extends PointcutVisitor { void visit(AnnotatePc pc) throws WeaveException; }
Note that the type PcVisitor2
has been used in the
accept
method in AnnotatePc
shown above.
Then, we modify the visitor classes implementing the
PointcutVisitor
interface.
@Refine static class Prepare2 extends PrepareVisitor { public Prepare2() { super(null); } public void visit(AnnotatePc pc) throws WeaveException { pc.prepare(parser); } }
The visit
method in the PrepareVisitor
class just calls the prepare
method
in the AnnotatePc
class.
@Refine static abstract class Match2 extends Matcher { 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 static class CallMatch2 extends CallMatcher { public CallMatch2() { super(null); } 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 static class FieldMatch2 extends FieldMatcher { public FieldMatch2() { super(null); } 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); } } }
Note that the value of the joinPoint
field is used
within the visit
method in the CallMatch2
and
FieldMatch2
classes.
joinPoint
is a field declared in the CallMatcher
and FieldMatcher
classes.
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 static class Cflow2 extends CflowCollector { public Cflow2() { super(null); } 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:
@Refine static class Pcut2 extends Pointcut { Pcut2() { super(null); } public Pointcut annotate(String tag) { setTree(new AnnotatePc(tag)); return this; } }
The annotate
method first makes an instance of
the AnnotatePc
class that
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
.
We must also define the annotate
method in the
Pcd
class.
@Refine static class Pcd2 extends Pcd { public static Pointcut annotate(String tag) { Pcut2 pc = (Pcut2)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 Pcut2
shown above.
Otherwise, we could not call the annotate
method that we added
to 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.