« Sam Pullara on VM-level AOP (AOSD 2005) | Main | Aspect library discussion at AOSD 2005 »

March 21, 2005

Per-instance aspects

A lot of people have asked for the code I showed in the AspectJ BOF at the AOSD conference last week for implementing per-instance aspect associations. The basic problem is to associate aspect behaviour with object instances on an individual basis. JBoss often cite their caching scheme as a use case for this - objects are associated with the caching aspect when they are added to the cache, but not before. For this reason, I'll use a (very simple) caching aspect for the example in this entry.

Imagine some class, Worker that produces a cachable result:

public class Worker { private static int counter = 100; @Caching.CachableResult public Integer doWork() { return new Integer(counter++); } }

Note that every time doWork is called on a Worker instance the value of a single static counter is returned and then incremented. The doWork method is annotated with the @Caching.CachableResult annotation indicating that its result may be cached.

What we'd like is a way to associate individual worker objects with a caching aspect, so that we can cache their results. Here's a test case from my test suite that should give you the idea:

public void testInstanceCaching() { Worker w1 = new Worker(); Worker w2 = new Worker(); assertEquals(new Integer(100), w1.doWork()); assertEquals(new Integer(101), w2.doWork()); assertEquals(new Integer(102), w1.doWork()); w1.associateAspect(Caching.class); assertEquals(new Integer(103),w1.doWork()); // cache miss assertEquals(new Integer(103),w1.doWork()); // cache hit assertEquals(new Integer(104),w2.doWork()); // no caching on w2 w2.associateAspect(Caching.class); assertEquals(new Integer(105),w2.doWork()); // cache miss assertEquals(new Integer(105),w2.doWork()); // cache hit assertEquals(new Integer(103),w1.doWork()); // cache hit w1.disassociateAspect(Caching.class); assertEquals(new Integer(106),w1.doWork()); // no more caching for w1 assertEquals(new Integer(105),w2.doWork()); // w2 still cached w1.associateAspect(Caching.class); assertEquals(new Integer(103),w1.doWork()); // caching enabled again }

Note the calls to associateAspect and disassociateAspect being made on instances of the Worker class. In JBoss you "prepare" a class for per-instance advising (weave hooks into all of the class's join points of interest) when it is loaded. In the scheme I'm implementing here, you do the following:

aspect DeclareWorkerInstanceAdvisable { declare parents : Worker implements PerInstanceAdvisable; }

The PerInstanceAdvisable interface is very simple (and something we should include in the AspectJ library):

public interface PerInstanceAdvisable { public void associateAspect(Class aspectType); public void disassociateAspect(Class aspectType); static aspect DefaultImplementation { public void PerInstanceAdvisable.associateAspect(Class aspectType) {} public void PerInstanceAdvisable.disassociateAspect(Class aspectType) {} } }

Note the trick of declaring a default implementation of an interface as a static inner aspect of the interface itself. I personally think this is a very elegant idiom.

So far so good - now it's time to look at the actual caching aspect implementation. First off the PerInstanceAssociationAspect super-aspect that can serve as the base class for any aspect participating in per-instance associations.

/** * Super-aspect for all aspects implementing per-instance associations. One aspect * instance will be created for each associated object. */ public abstract aspect PerInstanceAssociationAspect perthis(aspectAssociation()) { /** * This flag controls whether or not this instance of the aspect is * "active". An aspect becomes active when it is associated with * an instance, an inactive if it is disassociated again. */ protected boolean enabled = true; /** * Sub-aspects override this pointcut to match if the Class argument * at an aspectAssocation() join point matches their type. The * overriden version should look something like this: * protected pointcut isThisAspect(Class aspectType) : * args(aspectType) && if (aspectType.equals(MyAspect.class)); * where "MyAspect" is the type of the sub-aspect. */ protected abstract pointcut isThisAspect(Class aspectType); /** * A call to associateAspect with our aspect type passed as the * argument */ pointcut aspectAssociation() : execution(* PerInstanceAdvisable.associateAspect(Class)) && isThisAspect(Class); /** * A call to disassociateAspect. The implicit scoping given by the perthis * clause means that this only matches at join points where the object * executing the disassociateAspect operation is the the object with * which this aspect instance is associated. */ pointcut aspectDisassociation() : execution(* PerInstanceAdvisable.disassociateAspect(Class)); /** * Enable this aspect whenever it is associated. The implicit scoping * from the perthis clause restricts this advice to the object instance * with which we are associated only. */ after() returning : aspectAssociation() { enabled = true; } /** * Disable this aspect whenever it is disassociated. The implicit scoping * from the perthis clause restricts this advice to the object instance * with which we are associated only. */ after() returning : aspectDisassociation() { enabled = false; } }

I've ajdoc'd the aspect so hopefully it's self-explanatory from the comments.

Finally, here's the Caching sub-aspect that I promised right at the start of this article.

/** * A Caching aspect that caches the results of @CachableResult * operations, on a per-instance basis. PerInstanceAdvisable objects * may be associated with the cache by calling obj.associateAspect(Caching.class). * They may be disassociated from the cache by calling * obj.disassociateAspect(Caching.class). */ public aspect Caching extends PerInstanceAssociationAspect { /** * The guard that ensures advice never excutes for an object that * has previously been associated with the cache (causing this * aspect instance to be created), but has subsequently been * disassociated and not re-associated. */ private static boolean enabledFor(Object cachee) { return hasAspect(cachee) && aspectOf(cachee).enabled; } /** * The concrete definition of isThisAspect from the super-aspect. * Matches when the user tries to associate an aspect of type * Caching .class */ protected pointcut isThisAspect(Class aspectType) : args(aspectType) && if (aspectType.equals(Caching.class)); // ----- // the remainder of this aspect is particular to caching... /** * Marker annotation to be used by operations that produce * Cachable results; */ public @interface CachableResult {} private Cache cache = new Cache(); /** * A cached operation is the execution of any method that has * the @CachableResult annotation and an enabled caching * aspect. */ pointcut cachedOperation(Object cachee) : execution(@CachableResult * *(..)) && this(cachee) && if (enabledFor(cachee)); /** * Basic caching scheme... use the cached result if we * have one, otherwise proceed with the operation and * cache the result for next time. */ Object around(Object obj ) : cachedOperation(obj) { Object result = null; if (cache.hasResultFor(thisJoinPointStaticPart)) { // cache hit result = cache.getResultFor(thisJoinPointStaticPart); } else { // cache miss result = proceed(obj); cache.addResult(thisJoinPointStaticPart,result); } return result; } /** * Trivial Cache implementation using a Map. Note that JoinPoint.StaticPart makes * a stable key for caching the results of operations at join points. */ private static class Cache { Map cachedResults = new HashMap(); public boolean hasResultFor(JoinPoint.StaticPart jpsp) { return cachedResults.containsKey(jpsp); } public Object getResultFor(JoinPoint.StaticPart jpsp) { return cachedResults.get(jpsp); } public void addResult(JoinPoint.StaticPart jpsp, Object result) { cachedResults.put(jpsp,result); } } }

A weakness of this scheme as opposed to the JBoss implementation is that aspects intended to be used on a per-instance basis (such as the caching aspect above) have to be written with that intention in mind (whereas I believe that in JBoss you can associate any aspect with instances of a pre-prepared class). On the plus side, from my understanding of the differing implementations, this solution should perform better than the JBoss implementation.

Posted by adrian at March 21, 2005 04:07 PM [permalink]

Comments

I think you JUnit is incorrect...
w1.disassociateAspect(Caching.class);
assertEquals(new Integer(104),w1.doWork()); // no more caching for w1, since the aspect was around, it should be 104
assertEquals(new Integer(105),w2.doWork()); // w2 still cached

w1.associateAspect(Caching.class);
// I would think that associating the aspect should clear out any existing cache, otherwise, you'd have old data
assertEquals(new Integer(105),w1.doWork()); // caching enabled again
assertEquals(new Integer(105),w1.doWork());

Posted by: sc [TypeKey Profile Page] at April 14, 2005 01:09 PM

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?