How to extend GluonJ

Shigeru Chiba and Muga Nishizawa

Next page


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 and Pointcut classes.

  • Finally, install a @Glue implementing the new pointcut designator. It is done by modifying javassist.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 in residue)
    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.

Next page


Copyright (C) 2006 by Shigeru Chiba and Muga Nishizawa. All rights reserved.