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 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 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 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.

Next page


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