« Using Eclipse 2.1, but want to use the latest AspectJ compiler? | Main | Implementing caching with AspectJ - part II »

June 22, 2004

Implementing caching with AspectJ - part I

Time for some code...

Often in talks we give examples of common aspects, such as caching, pooling, auditing, security, persistence, and so on. I thought I'd start a mini-series of blog entries that examine some of these common aspects in turn and show you how to implement them using AspectJ. Today I'm going to look at caching.

Before we can do any caching, we need something to cache. Here's a simple DataProvider class that has a couple of expensive operations in its interface:

/** * @author adrian at aspectprogrammer.org * * A mock data providing class that can be used to illustrate caching * techniques using AspectJ. */ public class DataProvider { private int multiplicationFactor = 0; private int expensiveToComputeValue = 0; public DataProvider(int seed) { multiplicationFactor = seed; expensiveToComputeValue = seed; } /** * expensiveOperation is a true function (it always * returns the same output value for a given input * value), but takes a long time to compute the * answer. */ public int expensiveOperation(int x) { try { Thread.sleep(1000); } catch (InterruptedException ex) {} return x * multiplicationFactor; } /** * The expensive to compute value is different each * time you ask for it. It also takes a long time to * compute. */ public Integer getExpensiveToComputeValue(int x) { try { Thread.sleep(1000); } catch (InterruptedException ex) {} return new Integer(x + expensiveToComputeValue++); } }

Before going any further let's write a simple set of test cases that we want to pass when caching is working. The tests look to see that the cache offers a performance speed-up, and that the returned values from the cache are correct.

** * @author adrian at aspectprogrammer.org * Verify that the cache is doing its job. */ public class CachingTest extends TestCase { private DataProvider provider; public void testExpensiveOperationCache() { long start100 = System.currentTimeMillis(); int op100 = provider.expensiveOperation(100); long stop100 = System.currentTimeMillis(); long start200 = System.currentTimeMillis(); int op200 = provider.expensiveOperation(200); long stop200 = System.currentTimeMillis(); long start100v2 = System.currentTimeMillis(); int op100v2 = provider.expensiveOperation(100); long stop100v2 = System.currentTimeMillis(); long start200v2 = System.currentTimeMillis(); int op200v2 = provider.expensiveOperation(200); long stop200v2 = System.currentTimeMillis(); long expectedSpeedUp = 500; // expect at least 0.5s quicker with cache assertTrue("caching speeds up return (100)", ((stop100 - start100) - (stop100v2 - start100v2)) >= expectedSpeedUp); assertTrue("caching speeds up return (200)", ((stop200 - start200) - (stop200v2 - start200v2)) >= expectedSpeedUp); assertEquals("cache returns correct value(100)",op100,op100v2); assertEquals("cache returns correct value (200)",op200,op200v2); assertTrue("cache does not give erroneous hits",op200 != op100); } public void testExpensiveToComputeValueCache() { long start1 = System.currentTimeMillis(); Integer val1 = provider.getExpensiveToComputeValue(5); long stop1 = System.currentTimeMillis(); long start2 = System.currentTimeMillis(); Integer val2 = provider.getExpensiveToComputeValue(5); long stop2 = System.currentTimeMillis(); long expectedSpeedUp = 500; // expect at least 0.5s quicker with cache assertTrue("caching speeds up return", ((stop1 - start1) - (stop2 - start2)) >= expectedSpeedUp); assertEquals("get cached value rather than incremented one",val1,val2); } /* * @see TestCase#setUp() */ protected void setUp() throws Exception { super.setUp(); provider = new DataProvider(100); } }

Now at last we're ready to introduce the first caching aspect. This is a straightforward aspect that hard-wires the cache implementation (such as it is), and the operations to be cached.

/** * @author adrian at aspectprogrammer.org * Illustrating the bare-bones of a caching implementation using AspectJ */ public aspect BogBasicHardWiredCache { private Map operationCache = new HashMap(); private Map expensiveToComputeValueCache = new HashMap(); pointcut expensiveOperation(int x) : execution(* DataProvider.expensiveOperation(int)) && args(x); /** * caching for expensive operation */ int around(int x) : expensiveOperation(x) { int ret = 0; Integer key = new Integer(x); if (operationCache.containsKey(key)) { Integer val = (Integer) operationCache.get(key); ret = val.intValue(); } else { ret = proceed(x); operationCache.put(key,new Integer(ret)); } return ret; } pointcut expensiveValueComputation(int x) : execution(* DataProvider.getExpensiveToComputeValue(int)) && args(x); /** * caching for expensive to compute value */ Integer around(int x) : expensiveValueComputation(x) { Integer ret = null; Integer key = new Integer(x); if (expensiveToComputeValueCache.containsKey(key)) { ret = (Integer) expensiveToComputeValueCache.get(key); } else { ret = proceed(x); expensiveToComputeValueCache.put(key,ret); } return ret; } }

The bog-basic hard-wired caching aspect keeps a separate cache (I've just used a HashMap, but a real implementation would use something more sophisticated) for the expensiveOperation values and for the expensiveToComputeValues. The actual caching is very simple. In both cases I use around advice to see if the cache has a value for the given key, and if so return it without executing the expensive method. If there is no value in the cache, I execute the method and then store the return value in the cache before giving it back to the caller. With this aspect in place, the test cases pass.

We can do better than this though. We probably want to make the actual cache implementation configurable, and we can capture the essence of the caching algorithm in a library aspect, which I've called org.aspectprogrammer.caching.SimpleCaching:

/** * @author adrian at aspectprogrammer.org * A simple caching aspect with configurable cache implementation. * Sub-aspects simply specify the cachedOperation pointcut. */ public abstract aspect SimpleCaching { private Map cache; /** * Use the Map interface as a good approximation to a * cache for this example. * The cache can be provided to the aspect via e.g. dependency * injection. */ public void setCache(Map cache) { this.cache = cache; } /** * We don't know what to cache (that's why this is an abstract * aspect), but we know we need a key to index the cached values * by. */ abstract pointcut cachedOperation(Object key); /** * This advice implements the actual caching of values and * cache lookups. */ Object around(Object key) : cachedOperation(key) { Object ret; if (cache.containsKey(key)) { ret = cache.get(key); } else { ret = proceed(key); cache.put(key,ret); } return ret; } }

This aspect assumes that dependency-injection of some kind (perhaps configuration via Spring as I've illustrated in previous posts) will be used to pass the aspect a cache implementation (I've been lazy and just used Map as the cache interface here). The aspect is abstract as it doesn't know what to cache (the cachedOperation pointcut is abstract), but it does know how to implement the cache-lookup and population algorithm.

To make the test cases pass using this library aspect we need to introduce a couple of concrete sub-aspects. Here's the caching aspect for the expensiveOperation:

/** * @author adrian at aspectprogrammer.org * Illustrating use of the SimpleCaching library aspect, * with the cache at the data provider. */ public aspect ExpensiveOperationCaching extends SimpleCaching { pointcut cachedOperation(Object key) : execution(int DataProvider.expensiveOperation(int)) && args(key); /** * this constructor is here simply to facilitate testing all * of the different cache implementations without having the * test case depend on any one. Normally this dependency would * be set from outside of the aspect (e.g. by Spring). */ public ExpensiveOperationCaching() { setCache(new java.util.HashMap()); } }

Because I used an "execution" pcd to define the cachedOperation pointcut, this cache will be on the "execution" (ie server) side of the expensiveOperation.

Here's the aspect that caches the expensive-to-compute value. This aspect uses a "call" pcd to define the cachedOperation pointcut, and so this cache will be on the "call" (client) side - possibly an important distinction if the operation is a remote call.

/** * @author adrian at aspectprogrammer.org * Illustrating use of the SimpleCaching library aspect, * with the cache on the client side (useful if e.g. the * data provider is remote). */ public aspect ExpensiveToComputeValueCaching extends SimpleCaching { pointcut cachedOperation(Object key) : call(Integer DataProvider.getExpensiveToComputeValue(int)) && args(key); /** * this constructor is here simply to facilitate testing all * of the different cache implementations without having the * test case depend on any one. Normally this dependency would * be set from outside of the aspect (e.g. by Spring). */ public ExpensiveToComputeValueCaching() { setCache(new java.util.HashMap()); } }

Notice how easy it is to reuse the library aspect? I think that's enough for one day. Tomorrow I'll show you some easy extensions to get per-client caches (each client has a seperate cache), per data-provider caches (when there are multiple distinct data providers), and even per-transaction caches (with a lower-case "t") that give a constant value for the duration of a transaction.

Posted by adrian at June 22, 2004 09:20 PM [permalink]

Comments

Adrian,

Nice example; just one thing though the method

public Integer getExpensiveToComputeValue

returns the same value for a given parameter when caching is turned on; whereas as it should be returning different values with each invocation. Caching should not be appled to methods like these.

Chandresh

Posted by: Chandresh at June 24, 2004 11:02 PM

The getExpensiveToComputeValue method was designed to simulate something like a stock quote or other service, where you can tolerate data that is out of for a certain period of time. Not a very convincing simulation I agree ;).

You'd obviously need to use a cache that had a time-based eviction policy for data like this so that cache entries expire after say 15 minutes.

Adrian

Posted by: Adrian at June 25, 2004 07:04 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?