AspectJ Tutorial Exercises

Organization

The exercises work with a figure editor together with JUnit test cases. They progress, as most users do in their adoption of AspectJ, from non-functional, development-only aspects to aspects which augment a deployed program with crosscutting features.

For each exercise, you will compile and run with the AspectJ runtime and the JUnit test cases. The JUnit tests verify your code by inspecting messages you send using the Log class which is in the support directory.

If you are using Eclipse, compiling your project is accomplished with the compile button to the left of the perspective pull-down menu in the toolbar. If you are using the command-line tools, then you should compile all the files in base.lst, as well as whatever aspect code you add for an exercise (figures/Answer1a.java, in this example):

$ ajc -Xlint -argfile base.lst figures/Answer1a.java

If you are using Eclipse, running one of the unit tests is accomplished by right clicking the test (in the tests package) and selecting "Run". If you are using the command-line tools, then just invoke java on the compiled test class, such as

$ java tests.Test

The default test, tests.Test, performs some rudimentary tests on figure elements, and so is a useful test to run periodically.

To complete each exercise, create a file containing your aspects and include the file in your compile (e.g., replacing figures/Answer1a.java above). Use the AspectJ documentation as a reference, particularly the quick-reference and semantics appendices of the programming guide. The compiler's -Xlint option may catch some common mistakes. If you get stuck, inspect the test case included for each exercise.


1. Log simple properties

a. Trace one method

Intstrument your code so that before the Point's setX method begins executing, you log the string "set". To do this, use the utility class Log (with an import from support.Log) and call

Log.log("set")

This will write the string "set", followed by a semicolon terminator, to the Log. For example, with your aspect enabled,

Point p1 = new Point(10, 100);
p1.setX(37);
System.out.println(Log.getString());

should print out "set;".

Test this with the JUnit test case tests.Test1a. Without adding any aspects, this test should fail:

$ ajc -argfile base.lst 
$ java tests.Test1a
..F.......
Time: 0.07
There was 1 failure:
1) testSetXPointLog(tests.Test1a)junit.framework.AssertionFailedError: expected:<set;> but was:<>
        at tests.Test1a.testSetXPointLog(Test1a.java:30)
        at tests.Test1a.main(Test1a.java:16)

FAILURES!!!
Tests run: 9,  Failures: 1,  Errors: 0

But with the proper aspect added to the compilation, (in this case, figures/Answer1a.java, but you should feel free to use more evocative names), the test should pass

$ ajc -argfile base.lst figures/Answer1a.java
$ java tests.Test1a
.........
Time: 0.089

OK (9 tests)

Make sure to pass tests.Test1a before continuing.

b. Trace multiple methods

Continue your aspect to capture execution of more methods.

When Point.setX or Point.setY execute, you should log "set". When one of

Point.getX, Point.getY, Line.getP1, Line.getP2

execute, you should log "get".

Feel free to use wildcards in your pointcuts.

For example,

Point p1 = new Point(10, 100);
p1.setY(p1.getX());
System.out.println(Log.getString());

should print out "get;set;".

Make sure to pass the JUnit test tests.Test1b before continuing.

c. Use join point information

Instead of logging "set" and "get", log the name of the executing method. For example,

Point p1 = new Point(10, 100);
p1.setY(p1.getX());
System.out.println(Log.getString());

would print out "getX;setY;".

Make sure to pass the JUnit test tests.Test1c before continuing.

d. Refactor and extend

At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them. If the people to your left and right are doing fine, then think about refactoring your logging aspect from part c. You could separate your logging aspect into two aspects, one encapsulating the logging protocol, and the other encapsulating the choice of join points. See if there are more interesting refactorings to do.

In your refactoring, you make sure you continue to pass the JUnit test tests.Test1c.


2. More advanced Tracing

If you did some refactoring in the last exercise, restore your packages to their original contents (which were saved in the backup directory wherever you unpacked figures.zip) before starting the next exercises.

a. Tracing external calls

It can often be useful to trace only those calls that are made to outside code. Write an aspect to log calls to methods in the java.util package from the figures package.

You should send a short description of the join point to the log, with

Log.log(thisJoinPointStaticPart.getSignature().toShortString());

For example, if you compiled your aspect with this Main class

package figures;

class Main {
    public static void Main(String[] args) {
        java.util.Set s = new java.util.HashSet();
        s.add(null);
        s.clear();
        System.out.println(Log.getString());
    }
}

running it would print out "Set.add(Object);Set.clear();".

Make sure to pass the JUnit test tests.Test2a before continuing.

b. Logging exceptions thrown

Instead of logging whenever we make an external call to method from java.util, modify your code to log any Throwable thrown from such a call from code in the figures and test packages.

For example, if you compiled your aspect with this Main class

package figures;

class Main {
    public static void Main(String[] args) {
        java.util.Vector v = new java.util.Vector();
        try {
            v.get(1);
        } catch (ArrayIndexOutOfBoundsException e) { }
        System.out.println(Log.getString());
    }
}

running it would print out "caught java.lang.ArrayIndexOutOfBoundsException;".

Make sure to pass the JUnit test tests.Test2b before continuing.

c. Exposing caller and callee

As an extremely artifical example leading up to the next exercise, write an aspect that writes to the log:

Log the size of the calling group and the p0 point of the target box.

For example, if you compiled your aspect with this Main class

package figures;

class Main {
    public static void Main(String[] args) {
        Group g = new Group(new Point(30, 70));
        Box b = new Box(5, 5, 10, 10);
        g.add(b);
        g.move(100, 100); // logs "2Point(5, 5)" 
        g.add(new Point(1, 99));
        g.move(-2, -10); // logs "3Point(105, 105)" 
        System.out.println(Log.getString());
    }
}

running it would print out

2Point(5, 5);3Point(105,105);

As you may already have noticed, looking at the included JUnit tests will also help with the problems.

Make sure to pass the JUnit test tests.Test2c before continuing.

d. Tracing only in a control flow

Write an aspect that logs when the _x and _y fields on Point are set, but only when in the process of moving a Box.

Make sure to pass the JUnit test tests.Test2d before continuing.

e. Enabling per-object tracing

Write an aspect that allows the specification of individual figure elements whose move method should be logged. This will involve associating a boolean value with every figure element. Use around advice to intercept calls to Log.traceObject(Object) (which otherwise just signals an exception) to register the passed object as traceable. Then, write advice that logs the class name of any such moved object, but only if the object is registered as being traced. You may wish to use the if pointcut.

The Log class has a method that will log the short name of a Class minus its package name. Since this is the form expected by the test case, you should probably use support.Log.logClassName(Class).

Make sure to pass the JUnit test tests.Test2e before continuing.

f. Refactor and extend

At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them. If the people to your left and right are doing fine, then think about refactoring any of the aspects or the system in general, perhaps starting by merging part d and e.

In your refactoring, make sure you continue to pass appropriate the JUnit tests.


3. Check Invariants

If you did some refactoring in the last exercise, restore your packages to their original contents (which you should be able to do by copying the contents of backup) before starting the next exercises.

a. Check static invariants

One common coding convention is that no private field should be set outside of setter methods or constructors. Write an aspect to warn at compile time when such an illegal assignment expression exists.

You will find that the convention is violated twice in the figures package, but you may or may not agree that the "violation" is bad style. Deal with this information, either by making your aspect more nuanced by relaxing the coding convention, or by fixing the code to remove the violation.

Make sure your program still passes the JUnit test tests.Test (which it should also pass at the beginning of all exercises) before continuing.

b. Check preconditions

Write an aspect to throw an IllegalArgumentException whenever an attempt is made to set one of Point's int fields to a value that is either too large (greater than FigureElement.MAX_VALUE) or too small (less than FigureElement.MIN_VALUE).

Make sure to pass the JUnit test tests.Test3b before continuing.

c. Assure input

Instead of throwing an exception when one of Point's int fields are set to an out-of-bounds value, write an aspect to trim the value into an in-bounds one.

Make sure to pass the JUnit test tests.Test3c before continuing.

d. Check postconditions

There is a method on the Box class, void checkBoxness(), that checks whether the four points making up a box are correctly positioned relative to each other. Write an aspect that will make sure that after all calls to public methods of Box return, the checkBoxness() method is called.

When testing this aspect, you may find yourself facing a StackOverflowException. If so, carefully look at your pointcuts.

You may also see an irrecoverable stack overflow occurring, which may either show as such or simply abort silently, depending on your JVM. In this case, you should also check carefully to see what you're doing wrong, but here's a hint: The following program probably has the same behavior:

class Test {
    public static void main(String[] args) {
        try {
            main(args);
        } finally {
            main(args);
        }
    }
}

Make sure to pass the JUnit test tests.Test3d before continuing.

e. Check postconditions

FigureElements support the getBounds() method that returns a java.awt.Rectangle representing the bounds of the object. An important postcondition of the move operation is that the figure element's bounds rectangle should move by the same amount as the figure itself. Write an aspect to check for this postcondition.

You can create a copy of a figure element's bounds rectangle with new Rectangle(fe.getBounds()), and you can move a bounds rectangle rect with rect.translate(dx, dy).

Make sure to pass the JUnit test tests.Test2f before continuing.

f. Refactor and extend

At this point, check the people to your left and right. If they're stuck somewhere, see if you can help them. If the people to your left and right are doing fine, then think about refactoring any of the aspects or the system in general.

In your refactoring, make sure you continue to pass appropriate the JUnit tests.


4. Add crosscutting functionality

If you did some refactoring in the last exercise, restore your packages to their original contents (which were saved in the backup directory wherever you unpacked figures.zip) before starting the next exercises.

a. Add Colorizing Support

Figure elements currently have a hard-coded color. The first part of crosscutting functionality you will add will be support for figureElements to have settable line and fill colors. We have provided a stub aspect figures.ColorControl for you to fill in; that aspect's static methods are called from the JUnit test tests.Test4a.

Make sure to pass the JUnit test tests.Test4a before continuing.

b. Caching group bounds part 1: Manage enclosing groups

The second crosscutting feature you will be adding is support from caching the bound objects of Group figure elements, which may be costly to compute. Before starting that, write a tracing aspect to log all of a point's enclosing group identifiers whenever that point moves.

You need only deal with points that are added directly to Groups. You should handle those points contained in Lines and Boxes only if time permits.

Make sure to pass the JUnit test tests.Test4b before continuing.

c. Caching group bounds part 2: cache and invalidate

Now that you have a structure in place to maintain information about a point's enclosing groups, cache group's bounds rectangle so that repeated calls to getBounds() returns the same rectangle. However, whenever a point moves it should invalidate the caches of all enclosing groups.

Make sure to pass the JUnit test tests.Test4b before continuing.

d. Refactor and Extend

Congratulations! As before, check the people to your left and right and see if they're stuck. Otherwise, do any other refactorings that would make the code more robust or easier to maintain. Consider whether abstracting out parts of the gui-handling code would improve modularity. Bring back some of your tracing aspects from exercise 1 and explore the newly modularized system.