(The code for this example is in
.)
InstallDir
/examples/tracing
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)