Implementing Caching with AspectJ
Often in talks we give examples of common aspects, such as caching, pooling, auditing, security, persistence, and so on. Learn how to implement them using AspectJ.
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? In Part 2 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.
Part 2
I promised in Part 1 to show some variants on the basic caching scheme I showed. We are already able to introduce a simple client-side or server-side cache for an operation by reusing the org.aspectprogrammer.caching.SimpleCaching aspect. In Part II, I'll show you how to create per-client, per-data source, and per-transaction caches with very little effort too.
Here's how you would specify a per-client cache. What we're after here is that each client has their own private cache (perhaps the returned values are in some way particular to the client):
/** * @author adrian at aspectprogrammer.org * Illustrates use of SimpleCaching library aspect to implement a * (client-side) per-client cache. */ public aspect IndividualClientExpensiveOperationCaching extends SimpleCaching perthis(cachedOperation(Object)) { pointcut cachedOperation(Object key) : call(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 IndividualClientExpensiveOperationCaching() { setCache(new java.util.HashMap()); } }
The key difference in this aspect is that we specified that a unique aspect instance should be created perthis(cachedOperation(Object). This means that we get one caching aspect for each unique object that calls the DataProvider. In other words, one cache per client.
In a similar vein, we may have multiple instances of the DataProvider, and want to provide a per-data provider cache (perhaps each data provider returns different values). This is easily achieved with a pertarget clause:
/** * @author adrian at aspectprogrammer.org * Illustrates use of the SimpleCaching library aspect to provide * a client-side data-provider specific cache. * (Use execution in place of call in the cachedOperation pcut definition * for a server-side cache). */ public aspect DataProviderSpecificExpensiveOperationCaching extends SimpleCaching pertarget(cachedOperation(Object)){ pointcut cachedOperation(Object key) : call(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 DataProviderSpecificExpensiveOperationCaching() { setCache(new java.util.HashMap()); } }
This creates a new caching aspect instance for each unique target of a call to the DataProvider. In other words, for each DataProvider instance.
The final variant I will discuss centers around caching the getExpensiveToComputeValue return value within a transaction. I don't mean transaction in the JTA sense, simply the control flow of a given method. Recall that the implementation of getExpensiveToComputeValue returns a different answer each time it is called. If the per-transaction cache is working correctly, then within a single transaction (control-flow), the client wil see a constant return value no matter how many times the operation is called. Once the transaction is over, clients will see an updated value the next time the operation is called. A sample test case might make this clearer...
Here's a client that gets the expensive value a couple of times in the control flow of the getExpensiveValues method.
public class ProviderClient { DataProvider provider; public ProviderClient(DataProvider p) { this.provider = p; } public Integer[] getExpensiveValues(int x) { Integer a = getIt(x); Integer b = getIt(x); return new Integer[] {a,b}; } private Integer getIt(int x) { return provider.getExpensiveToComputeValue(x); } }
Now, here's the test case that we want to pass when the caching is working correctly:
public class PerTransactionCachingTest extends TestCase { public void testCaching() { ProviderClient c = new ProviderClient(new DataProvider(100)); Integer[] firstTime = c.getExpensiveValues(4); Integer[] secondTime = c.getExpensiveValues(4); assertEquals("cached in tx",firstTime[0],firstTime[1]); assertEquals("cached in tx",secondTime[0],secondTime[1]); assertTrue("not cached across tx", firstTime[0].intValue() != secondTime[0].intValue()); } }
Finally, here's the simple caching aspect that implements this policy:
/** * @author adrian at aspectprogrammer.org * Illustrates use of SimpleCaching library aspect to give a * cache that returns a constant value for the duration of a * given "transaction" (method execution control flow). */ public aspect PerTransactionExpensiveToComputeValueCaching extends SimpleCaching percflow(transaction()){ pointcut transaction() : execution(* ProviderClient.getExpensiveValues(int)); 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 PerTransactionExpensiveToComputeValueCaching() { setCache(new java.util.HashMap()); } }
Notice that the aspect is specified "percflow," meaning that we get a unique aspect instance for every control flow beginning at a join point matched by the transaction pointcut. This is defined to be simply whenever we execute the getExpensiveValues method in the ProviderClient. With this aspect in place the test passes.
I wrote all the test cases and aspects for part I and part II of these blog entries using AJDT (what else :) ? ), and have a "caching" Eclipse project that contains all of the code. I'll zip this up and make it available in the "Code Samples" section of the aspectprogrammer.org website soon.
About the author
Author:Adrian Colyer
Blog: http://www.aspectprogrammer.org
Adrian leads the open source AspectJ and AspectJ Development Tools (AJDT) projects on Eclipse.org. He is a frequent writer and presenter on the topic of aspect-oriented programming, and is based at the IBM Hursley development laboratory in the UK. In addition to "The Aspects Blog," he maintains a small site on AOP, http;//www.aspectprogrammer.org, where you can find a collection of articles and papers.