Reusable Aspects

Tracing using Aspects, Revisited

(The code for this example is in InstallDir/examples/tracing.)

Tracing—Version 3

One advantage of not exposing the methods traceEntry and traceExit as public operations is that we can easily change their interface without any dramatic consequences in the rest of the code.

Consider, again, the program without AspectJ. Suppose, for example, that at some point later the requirements for tracing change, stating that the trace messages should always include the string representation of the object whose methods are being traced. This can be achieved in at least two ways. One way is keep the interface of the methods traceEntry and traceExit as it was before,

  public static void traceEntry(String str);
  public static void traceExit(String str);

In this case, the caller is responsible for ensuring that the string representation of the object is part of the string given as argument. So, calls must look like:

  Trace.traceEntry("Square.distance in " + toString());

Another way is to enforce the requirement with a second argument in the trace operations, e.g.

  public static void traceEntry(String str, Object obj);
  public static void traceExit(String str, Object obj);

In this case, the caller is still responsible for sending the right object, but at least there is some guarantees that some object will be passed. The calls will look like:

  Trace.traceEntry("Square.distance", this);

In either case, this change to the requirements of tracing will have dramatic consequences in the rest of the code -- every call to the trace operations traceEntry and traceExit must be changed!

Here's another advantage of doing tracing with an aspect. We've already seen that in version 2 traceEntry and traceExit are not publicly exposed. So changing their interfaces, or the way they are used, has only a small effect inside the Trace class. Here's a partial view at the implementation of Trace, version 3. The differences with respect to version 2 are stressed in the comments:

abstract aspect Trace {

    public static int TRACELEVEL = 0;
    protected static PrintStream stream = null;
    protected static int callDepth = 0;

    public static void initStream(PrintStream s) {
        stream = s;
    }

    protected static void traceEntry(String str, Object o) {
        if (TRACELEVEL == 0) return;
        if (TRACELEVEL == 2) callDepth++;
        printEntering(str + ": " + o.toString());
    }

    protected static void traceExit(String str, Object o) {
        if (TRACELEVEL == 0) return;
        printExiting(str + ": " + o.toString());
        if (TRACELEVEL == 2) callDepth--;
    }

    private static void printEntering(String str) {
        printIndent();
        stream.println("Entering " + str);
    }

    private static void printExiting(String str) {
        printIndent();
        stream.println("Exiting " + str);
    }

    private static void printIndent() {
        for (int i = 0; i < callDepth; i++)
            stream.print("  ");
    }

    abstract pointcut myClass(Object obj);

    pointcut myConstructor(Object obj): myClass(obj) && execution(new(..));
    pointcut myMethod(Object obj): myClass(obj) &&
        execution(* *(..)) && !execution(String toString());

    before(Object obj): myConstructor(obj) {
        traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
    }
    after(Object obj): myConstructor(obj) {
        traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
    }

    before(Object obj): myMethod(obj) {
        traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
    }
    after(Object obj): myMethod(obj) {
        traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
    }
}

As you can see, we decided to apply the first design by preserving the interface of the methods traceEntry and traceExit. But it doesn't matter—we could as easily have applied the second design (the code in the directory examples/tracing/version3 has the second design). The point is that the effects of this change in the tracing requirements are limited to the Trace aspect class.

One implementation change worth noticing is the specification of the pointcuts. They now expose the object. To maintain full consistency with the behavior of version 2, we should have included tracing for static methods, by defining another pointcut for static methods and advising it. We leave that as an exercise.

Moreover, we had to exclude the execution join point of the method toString from the methods pointcut. The problem here is that toString is being called from inside the advice. Therefore if we trace it, we will end up in an infinite recursion of calls. This is a subtle point, and one that you must be aware when writing advice. If the advice calls back to the objects, there is always the possibility of recursion. Keep that in mind!

In fact, esimply excluding the execution join point may not be enough, if there are calls to other traced methods within it -- in which case, the restriction should be

&& !cflow(execution(String toString()))

excluding both the execution of toString methods and all join points under that execution.

In summary, to implement the change in the tracing requirements we had to make a couple of changes in the implementation of the Trace aspect class, including changing the specification of the pointcuts. That's only natural. But the implementation changes were limited to this aspect. Without aspects, we would have to change the implementation of every application class.

Finally, to run this version of tracing, go to the directory examples and type:

ajc -argfile tracing/tracev3.lst

The file tracev3.lst lists the application classes as well as this version of the files Trace.java and TraceMyClasses.java. To run the program, type

java tracing.version3.TraceMyClasses

The output should be:

  --> tracing.TwoDShape(double, double)
  <-- tracing.TwoDShape(double, double)
  --> tracing.Circle(double, double, double)
  <-- tracing.Circle(double, double, double)
  --> tracing.TwoDShape(double, double)
  <-- tracing.TwoDShape(double, double)
  --> tracing.Circle(double, double, double)
  <-- tracing.Circle(double, double, double)
  --> tracing.Circle(double)
  <-- tracing.Circle(double)
  --> tracing.TwoDShape(double, double)
  <-- tracing.TwoDShape(double, double)
  --> tracing.Square(double, double, double)
  <-- tracing.Square(double, double, double)
  --> tracing.Square(double, double)
  <-- tracing.Square(double, double)
  --> double tracing.Circle.perimeter()
  <-- double tracing.Circle.perimeter()
c1.perimeter() = 12.566370614359172
  --> double tracing.Circle.area()
  <-- double tracing.Circle.area()
c1.area() = 12.566370614359172
  --> double tracing.Square.perimeter()
  <-- double tracing.Square.perimeter()
s1.perimeter() = 4.0
  --> double tracing.Square.area()
  <-- double tracing.Square.area()
s1.area() = 1.0
  --> double tracing.TwoDShape.distance(TwoDShape)
    --> double tracing.TwoDShape.getX()
    <-- double tracing.TwoDShape.getX()
    --> double tracing.TwoDShape.getY()
    <-- double tracing.TwoDShape.getY()
  <-- double tracing.TwoDShape.distance(TwoDShape)
c2.distance(c1) = 4.242640687119285
  --> double tracing.TwoDShape.distance(TwoDShape)
    --> double tracing.TwoDShape.getX()
    <-- double tracing.TwoDShape.getX()
    --> double tracing.TwoDShape.getY()
    <-- double tracing.TwoDShape.getY()
  <-- double tracing.TwoDShape.distance(TwoDShape)
s1.distance(c1) = 2.23606797749979
  --> String tracing.Square.toString()
    --> String tracing.TwoDShape.toString()
    <-- String tracing.TwoDShape.toString()
  <-- String tracing.Square.toString()
s1.toString(): Square side = 1.0 @ (1.0, 2.0)