« 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 { MapcachedResults = 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 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.)