« Printing entries from this blog | Main | Burton analyst report on AOP now available »

April 12, 2005

Aspects as automaton

This post is inspired by discussions on the aspectj-users list over the last month discussing the best way to implement aspects that police rules based on sequences of join points in the execution of a program. Thanks go to Eric Bodden and others for provoking the discussion. What follows is my write-up of how I would tackle the problem using AspectJ 5.

We'll need a requirement to work from. Let's take good old foobar. Everyone knows that foo comes before bar. What if we wanted to write an aspect to make sure that was true? More precisely, let's write an aspect to enforce the following rule:

Every call to bar() within an action must be preceded by at least one call to foo(). After any call to bar(), foo() must be called at least once before bar() can be called again.

Here's a sample program to illustrate the point

public class CommandProcessor { public static void main(String[] args) { CommandProcessor cp = new CommandProcessor(); cp.doAction1(); cp.doAction2(); cp.doAction3(); } public void doAction1() { foo(); bar(); // ok, preceded by foo() foo(); } public void doAction2() { foo(); foo(); bar(); // ok, preceded by foo(); bar(); // not ok, must call foo() at least once before every bar() } public void doAction3() { fooHelper(); barHelper(); barHelper(); // not ok, you can't get away with it even if the call to bar is indirect.... } private void fooHelper() { foo(); } private void barHelper() { bar(); } private void foo() {} private void bar() {} }

The following state transition diagram shows the possible states of a state machine to implement these checks. The BAR state colored in Red is the illegal state that we don't want to allow.

Some say there's no smoke without a fire. In our case there's...

            
public aspect NoBarWithoutFoo { .. }

Let's use an enum to capture the states in our transition diagram:

            
public aspect NoBarWithoutFoo { enum FooBarStateMachine { START,FOO,BAR,FOOBAR; } ... }

In fact, we can use the enum type to capture not just the states but the transitions too. We have two kinds of transition, foo(), and bar(). Here's the expanded implementation of the enum that fully encodes the STD.

            
public aspect NoBarWithoutFoo { enum FooBarStateMachine { // transition events public abstract FooBarStateMachine foo(); public abstract FooBarStateMachine bar(); // states START { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }, FOO { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return FOOBAR;} }, BAR { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }, FOOBAR { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }; } ... }

Anytime we transition into the BAR state, that's bad. We can write that rule straight into the aspect by adding the following declarations:

            
pointcut stateChange() : execution(FooBarStateMachine FooBarStateMachine.*(..)); after() returning(FooBarStateMachine newState) : stateChange() { if (newState == FooBarStateMachine.BAR) { // take appropriate action on entering an illegal state... System.err.println("Illegal state transition - bar without foo!"); } }

The final piece of the puzzle is to wire join points in the program execution into actions that fire the state transitions. The basic process is to write one pointcut for each transition event (foo() and bar() in this example, plus one pointcut to match join points that cause us to enter the START state).

            
/** * An action executed by the CommandProcessor */ pointcut doAction() : execution(* CommandProcessor.do*(..)); /** * Reset the state machine any time we enter a doXXX method in the CommandProcessor * unless we are calling the action as part of processing another action. */ pointcut enterStartState() : doAction() && !cflowbelow(doAction()); /** * Foo transition is any call to foo() made during action processing */ pointcut foo() : call(* CommandProcessor.foo(..)) && cflowbelow(doAction()); /** * Bar transition is any call to bar() made during action processing */ pointcut bar(): call(* CommandProcessor.bar(..)) && cflowbelow(doAction());

Almost there, we just need a copy of a state machine inside the aspect, and some advice to trigger the transitions on it.I'm going to use a ThreadLocal for the state machine. For this particular use case we could have used a regular variable declaration and made the aspect percflow but the ThreadLocal approach is a more general solution for cases when the transition events you are interested in are not necessarily bounded by a single control flow.

         
private ThreadLocal<FooBarStateMachine> stateMachine = new ThreadLocal<FooBarStateMachine>(); before() : enterStartState() { stateMachine.set(FooBarStateMachine.START); } before(): foo() { FooBarStateMachine newState = stateMachine.get().foo(); stateMachine.set(newState); } before() : bar() { FooBarStateMachine newState = stateMachine.get().bar(); stateMachine.set(newState); }

That completes the aspect. If we you compile and run this little example you'll see the output:

            
Illegal state transition - bar without foo! Illegal state transition - bar without foo!

The complete aspect implementation is below for reference. Note that whilst I've used this aspect-driven state machine approach for policy enforcement, it could well be a useful implementation technique for many state-transition based applications.

public aspect NoBarWithoutFoo { enum FooBarStateMachine { // transition events public abstract FooBarStateMachine foo(); public abstract FooBarStateMachine bar(); // states START { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }, FOO { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return FOOBAR;} }, BAR { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }, FOOBAR { public FooBarStateMachine foo() { return FOO; } public FooBarStateMachine bar() { return BAR; } }; } pointcut stateChange() : execution(FooBarStateMachine FooBarStateMachine.*(..)); after() returning(FooBarStateMachine newState) : stateChange() { if (newState == FooBarStateMachine.BAR) { // take appropriate action on entering an illegal state... System.err.println("Illegal state transition - bar without foo!"); } } private ThreadLocal<FooBarStateMachine> stateMachine = new ThreadLocal<FooBarStateMachine>(); /** * An action executed by the CommandProcessor */ pointcut doAction() : execution(* CommandProcessor.do*(..)); /** * Reset the state machine any time we enter a doXXX method in the CommandProcessor * unless we are calling the action as part of processing another action. */ pointcut enterStartState() : doAction() && !cflowbelow(doAction()); /** * Foo transition is any call to foo() made during action processing */ pointcut foo() : call(* CommandProcessor.foo(..)) && cflowbelow(doAction()); /** * Bar transition is any call to bar() made during action processing */ pointcut bar(): call(* CommandProcessor.bar(..)) && cflowbelow(doAction()); // state transitioning before() : enterStartState() { stateMachine.set(FooBarStateMachine.START); } before(): foo() { FooBarStateMachine newState = stateMachine.get().foo(); stateMachine.set(newState); } before() : bar() { FooBarStateMachine newState = stateMachine.get().bar(); stateMachine.set(newState); } }

Posted by adrian at April 12, 2005 08:20 PM [permalink]

Comments

Nice article!
I like the way you employ enums for this purpose. Looks like a pretty intuitive design this way.

I have added some comments on how this is related to perinstance aspects and state association with objects. Maybe people find this an interesting extension...

See here...

Posted by: Eric Bodden [TypeKey Profile Page] at April 15, 2005 09:03 AM

Post a comment

Thanks for signing in, . Now you can comment. (sign out)

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)


Remember me?