Caching in Java using Aspect Oriented Programming (AOP)
As the scale of web applications increases, performance optimization considerations are more frequently included in initial design. One optimization technique used extensively is caching. A cache contains pre-processed data that is ready to use without redoing the processing. Processing may include extensive computation and accessing data over a network or on disk.
Keep the details out of your code
One design consideration when introducing a caching mechanism in your code is to keep the details out of your code. Most caches are just simple key value stores, and so it’s tempting to introduce them where you have a performance issue. Imagine the java method.
public Shop getShopsNear(Shop shop) { Shop returnShop; // query google maps api with shop data to get region // query database for region // calculate center of region // sort by distance from center returnShop = closestShopToCenter; return returnShop; } |
Obviously that has the potential to take a long time to process. It can also be expected that there are a reasonably small number of shops. Even if there are several thousand, caching several thousand results in memory is very manageable.
Wrong way
It might be tempting to jump right in and introduce caching like this:
public Shop getShopsNear(Shop shop) { Shop returnShop; // create a cache key String cacheKey = "getShopsNear: " + shop.toString(); // check for cached result Cache cache = CacheProvider.getCache(); returnShop = cache.get(cacheKey); if (returnShop == null) { // query google maps api with shop data to get region // query database for region // calculate center of region // sort by distance from center returnShop = closestShopToCenter; // cache result cache.put(cacheKey, returnShop); } return returnShop; } |
At first glance, that’s great. It follows most quick start tutorials for caching solutions and it will improve the performance of your code. However, there are several problems with this approach. I highlight some here.
- Higher risk of key conflicts and debugging since it’s managed at the method level
- The code is tied to specific cache implementation
- The method is less clear due to clutter from caching
AOP – a better way
One way to get around the problems above and have a caching mechanism that will grow with your application and provide flexibility is to use Aspect Oriented Programming. Google Guice also refers to this as Method Interception. The idea is that you identify some pre-processing that should take place before calling the actual method.
In this case, we want to put a cache interceptor at the method level and keep the processing of caching, such as key generation, cache provider selection, etc. centralized. Here’s what that might look like.
@Cached(timeToLiveSeconds = 3600) public Shop getShopsNear(Shop shop) { Shop returnShop; // query google maps api with shop data to get region // query database for region // calculate center of region // sort by distance from center returnShop = closestShopToCenter; return returnShop; } |
The only change to your original method is the @Cached annotation. You also have the option of defining the duration of that data in the cache, or leave it out to use the default duration.
The method interceptor code deals with the selection of cache provider, key generation, etc. This makes cache evaluation fast and effortless. It also keeps your application code clear. Changes in the future are now easy to configure.
Getting Started
I’ve created an AOP caching for Guice project. Fork the code or use it as is.
References
My library is based on the initial work by bpoulson.