Caching via Spring AOP

Introduction

Performance has always been an emphasis in many software applications, and caching has been used frequently as a method to speed-up critical data points.

This article will teach you how to use Spring's AOP feature to inject caching (using ehcache) into your application.

Caching, A Simple Introduction

We shall start off with a simple introduction of caching. We have the following (typical) Data Manager definition.

public class DataManager {
  public Object get(Object key)   
  public void set(Object key, Object value) 
  public void remove(Object key) 
  public void clear() 

The Data Manager consist of typical operations like setting data, getting data, as well as clearing data. Mapping to database operations, you could think of get as 'select', set as both 'insert' and 'update', remove as 'delete', and clear as 'delete *'

The simplest form of caching to be done would be to integrate a hash map cache into the operations call themselves. It should look like the following code block:

import java.util.HashMap;

public class DataManager {
  private HashMap<Object, Object> cache = new HashMap<Object, Object>();
  public Object get(Object key) {
    if (cache.containsKey(key)) return cache.get(key);
	Object value = ... // get data
	cache.put(key, value);
	return value;
  }
  public void set(Object key, Object value) {
    // update data
	cache.put(key, value);
  }
  public void remove(Object key) {
    // remove data
	cache.remove(key);
  }
  public void clear() {
    // clear data
	cache.clear();
  }
}

Concurrency issues aside (refer to the article on "Locking via Spring AOP"), this approach has other 'problems'. You have to inject the caching functionality into the many possible critical data points you have, which are, in some sense, unnecessary replication of the same code functionality. The additional code also 'obscure' the true logic of the operation, which is data retrieval (or transformation in some cases).

Note that first off, this is a simple implementation. Ideally you should be using a 3rd party cache library, which provides functionality like cache size limitation, object removal policies like LRU (least recently used), cache overflow (to disk storage), etc.

We will first build on using ehcache, a more advanced caching library, to replace our simple caching implementation.

Using ehcache

ehcache's cache can be defined either via a configuration file, or created at runtime. For this article, we will take the approach of defining cache via a configuration file.

Below is the configuration file "ehcache.xml", defined in the root of the src folder.

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd">

  <diskStore path="java.io.tmpdir"/>

  <cacheManagerEventListenerFactory class="" properties=""/>

  <cacheManagerPeerProviderFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
      properties="peerDiscovery=automatic,
            multicastGroupAddress=230.0.0.1,
            multicastGroupPort=4446, timeToLive=1"
      propertySeparator=","
      />

  <cacheManagerPeerListenerFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"/>

  <defaultCache
      maxElementsInMemory="10000"
      eternal="false"
      timeToIdleSeconds="120"
      timeToLiveSeconds="120"
      overflowToDisk="true"
      diskSpoolBufferSizeMB="30"
      maxElementsOnDisk="10000000"
      diskPersistent="false"
      diskExpiryThreadIntervalSeconds="120"
      memoryStoreEvictionPolicy="LRU"
      />

  <cache name="taskCache"
       maxElementsInMemory="10000"
       eternal="false"
       overflowToDisk="false"
       timeToIdleSeconds="300"
       timeToLiveSeconds="600"
       memoryStoreEvictionPolicy="LFU"
      />

</ehcache>

Most of the fields are self-explainatory, and as the primary purpose of this article is Caching via Spring AOP, for more information, please visit ehcache's website.

That said, a few basic concept has to be explained.

There are three important object definitions within ehcache.

  • net.sf.ehcache.CacheManager: This is the 'entry point' of which you obtain pre-defined cache, or create one.

  • net.sf.ehcache.Cache: This is the cache from which you add, get, remove objects, identified by their keys.

  • net.sf.ehcache.Element: This is a cached element, which in a very basic definition, is a key/value pair.

With these basic definitions, let us go into implementing an ehcache in our DataManager.

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import net.sf.ehcache.CacheManager;

public class DataManager {
  private Cache cache = CacheManager.getInstance().getCache("mycache");
  public Object get(Object key) {
    Element e = cache.get(key);
    if (e != null) return e.getObjectValue();
	Object o = ... // get data
	cache.put(new Element(key, value));
	return value;
  }
  public void set(Object key, Object value) {
    // update data
    cache.put(new Element(key, value));
  }
  public void remove(Object key) {
    // remove data
	cache.remove(key);
  }
  public void clear() {
    // clear data
	cache.removeAll();
  }
}

The CacheManager can be used as a singleton, using CacheManager.getInstance. This reads the "ehcache.xml" and creates all the pre-defined caches. To put an object into the cache, an Element object has to be first created. The element object would contain the key/value to be cached. To check if an object is cached by the key, simply call Cache.get. If it returns a null, the object is not cached.

Aspect Oriented Programming

We can actually take advantage of AOP(Aspect Oriented Programming), to define a single aspect that we could apply caching on. This shifts the responsibility of caching out of the methods themselves, as well as result in clearer code, which helps in readablity and maintainablity of code.

To begin, let's define some annotations.

Marking Methods With Annotations

So, we need four annotations, one for setting data, one for getting data, one for removing data, and one for clearing all data.

package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetObjectOperation {
  // empty
}
package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PutObjectOperation {
  // empty
}
package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RemoveObjectOperation {
  // empty
}
package com.technoriment.data;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClearObjectsOperation {
  // empty
}

Now that we have the required annotations, we can mark our methods with them.

public class DataManager {
  @GetObjectOperation
  public Object get(Object key)   
  @PutObjectOperation
  public void set(Object key, Object value) 
  @RemoveObjectOperation
  public void remove(Object key) 
  @ClearObjectsOperation
  public void clear() 

Defining the Aspect

Our first task on hand now would be to define (or rather code) the aspect. As mentioned before, this article will be using Spring's AOP functionality.

public class CacheAspect {
  public final Object GetObject(Object key)   
  public final void PutObject(Object key, Object value)   
  public final void RemoveObject(Object key)   
  public final void ClearObjects()   
}

Before proceeding, we need to define some logical flow of the operations themselves, which would lead to furthur updates to the operation signatures.

When getting an object, the cache is first checked for the key. If it exists, the cached value is returned. If it does not, the operation is allowed to proceed as per normal flow, and the returned value is cached. Therefore we have identified this operation as an 'around advice', that is, we wrap the advised method, and based on a condition, to proceed or otherwise, as well as obtain the returned result.

After putting an object, if successfully, the object's key/value pair would be placed into the cache. On first glance, this seemed like an 'after advice', but an after advice does not have access to the parameters. Therefore this shall be an 'around advice' as well.

For both remove and clear, we would like the cache removing/clearing operations to happen before the actual operation flow. Therefore for the both of them they shall be 'before advice'.

And now for an updated definition:

public class CacheAspect {
  public final Object getObject(final ProceedingJoinPoint pjp) throws Throwable {
    Object o = null;
    final Object[] args = pjp.getArgs();
    final Object key = args[0];

    if (key != null) {
      Object o = ...// get object
      
      if (o != null) {
        return o;
      }
    }
    
    o = pjp.proceed();
    
    if (o != null) {
      // cache object
    }
    
    return o;
  }
  public final void removeObject(final Object key) {
    if (key != null) {
      // remove from cache
    }
  }
  public final Object putObject(final ProceedingJoinPoint pjp) throws Throwable {
    final Object[] args = pjp.getArgs();
    final Object key = args[0];
    final Object value = args[1];
    
    final Object result = pjp.proceed();
    
    if (key != null && value != null) {
      // update cache
    }
    
    return result;
  }
  public final void clearObjects() {
    // clear cache
  }
}

Who Provides The Cache?

A relatively simple and clear aspect. We did, however, hit an interesting point. Should the aspect provide the cache? It would be relatively restrictive if we force all advised objects to use the same cache, or all advised objects to use their own instance of cache. Such a decision is usually made on a case to case basis. Therefore we delegate the cache provider to be configured along with the aspect later, by providing an interface known as the CacheProvider.

public interface CacheProvider {
  Object get(final Object key);
  void put(final Object key, final Object value);
  void remove(final Object key);
  void clear();
}

And we update the aspect to take advantage of the provider.

public class CacheAspect {
  private CacheProvider cacheProvider;
  private final CacheProvider getCacheProvider() { return cacheProvider; }
  public final void setCacheProvider(final CacheProvider cacheProvider) { this.cacheProvider = cacheProvider; }
  public final Object getObject(final ProceedingJoinPoint pjp) throws Throwable {
    Object o = null;
    final Object[] args = pjp.getArgs();
    final Object key = args[0];
    
    if (key != null) {
      o = getCacheProvider().get(key);
      
      if (o != null) {
        return o;
      }
    }
    
    o = pjp.proceed();
    
    if (o != null) {
      getCacheProvider().put(key, o);
    }
    
    return o;
  }
  public final void removeObject(final Object key) {
    if (key != null) 
      
    
  }
  public final Object putObject(final ProceedingJoinPoint pjp) throws Throwable {
    final Object[] args = pjp.getArgs();
    final Object key = args[0];
    final Object value = args[1];
    
    final Object result = pjp.proceed();
    
    if (key != null && value != null) {
      getCacheProvider().put(key, value);
    }
    
    return result;
  }
  public final void clearObjects() 
    
  
}

Implementing the provider

We will now create an implementation of a cache provider. We will be using ehcache again.

import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;

public class EhcacheProvider implements CacheProvider {
  private Cache cache;
  private final Cache getCache() { return cache; }
  public final void setCache(final Cache cache) { this.cache = cache; }
  public final Object get(final Object key) {
    final Element e = getCache().get(key);
    if (e == null) return null;
    return e.getObjectValue();
  }
  public final void put(final Object key, final Object value) {
    getCache().put(new Element(key, value));
  }
  public final void remove(final Object key) 
    
  
  public final void clear() 
    
  
}

Weaving in the advice

We now have all the foundation work done! Time to weave the aspect in using Spring! Below is the configuration file used.


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

  <aop:config>
    <aop:aspect id="taskCacheAspect" ref="cacheAspect">
    
      <aop:around pointcut="@annotation(com.technoriment.data.GetObjectOperation)" method="getObject" />
      <aop:around pointcut="@annotation(com.technoriment.data.PutObjectOperation) and args(key, value)" method="putObject" />
      <aop:before pointcut="@annotation(com.technoriment.data.RemoveObjectOperation) and args(key)" method="removeObject" />
      <aop:before pointcut="@annotation(com.technoriment.data.ClearObjectsOperation)" method="clearObjects" />

    </aop:aspect>
  </aop:config>

  <bean id="cacheManager" class="net.sf.ehcache.CacheManager" scope="singleton">
  </bean>
  
  <bean id="taskCache" scope="singleton" factory-bean="cacheManager" factory-method="getCache">
    <constructor-arg value="taskCache" />
  </bean>
      
  <bean id="cacheProvider"
    class="com.technoriment.aspects.caches.ehcache.EhcacheProvider"
    autowire="byType"
    dependency-check="all"
    scope="singleton">

  </bean>
  
  <bean id="cacheAspect"
    class="com.technoriment.aspects.caches.CacheAspect"
    autowire="byType"
    dependency-check="all"
    scope="singleton">

  </bean>
  
  <bean id="target"
    class="com.technoriment.aspects.caches.tests.support.CacheAspectTarget"
    autowire="byType"
    scope="singleton">

  </bean>
  
</beans>

Dissecting the Configuration File

I defined a total of five Spring objects in the example configuration. First is the aspect(cacheAspect of type CacheAspect), a cache provider(cacheProvider of type EhcacheProvider), a cache manager(cacheManager of type CacheManager), a cache(taskCache, obtained from the cache manager), and finally, the advised object itself (target of type CacheAspectTarget).

We next define an aspect in the application.

<aop:config>
  <aop:aspect id="taskCacheAspect" ref="cacheAspect">
    ....
  </aop:aspect>
</aop:config>

Aspects are to be defined within a config section. Each aspect requires a unique id to be specified, and the ref fields indicate the Spring bean object to be used as the aspect object.

Now to actually advise the advised object!

<aop:around pointcut="@annotation(com.technoriment.data.GetObjectOperation)" method="getObject" />
<aop:around pointcut="@annotation(com.technoriment.data.PutObjectOperation) and args(key, value)" method="putObject" />
<aop:before pointcut="@annotation(com.technoriment.data.RemoveObjectOperation) and args(key)" method="removeObject" />
<aop:before pointcut="@annotation(com.technoriment.data.ClearObjectsOperation)" method="clearObjects" />

These are the four advices we are applying on the advised object. A pointcut is an expression to be matched by Spring, that the advice will be applied on. We see two forms of advice types here: before and around. They are quite clear, in that a before advice would be applied before a method call, and an around advice would be wrapped around a method call. There are more advice types, but for the purpose of this article, these two will be sufficient. Once the advised method is triggered (the stage of trigger depends on the type, before or around), the specified method in the field "method" will be invoked.

The pointcut expression we used here are fairly simple. For example, the following

@annotation(com.technoriment.data.PutObjectOperation) and args(key, value)

The first part indicates that all methods marked with an annotation of PutObjectOperation will be matched. The second part has two meanings. First it adds an additional matching expression that the object that will be advised must also contain two arguments. Notice the use of the words key and value, which are actually the parameter names of the method to be invoked in the aspect. Spring will lookup the method for the type of parameters the advised object must have. Next, it tells Spring to bind the parameters as parameters to the aspect method.

And this is, in a nutshell, how to weave the advice in!

Conclusion

As we can see, using an AOP approach actually results in more 'dynamic configurations', reduce 'unnecessary' logic within the application itself, resulting in more easily maintainable code.

We could swap in different cache implementations easier and propagate the change easily to all classes, or we could stop using caching all together on applications that do not require them. Code change impact is kept to a bare miminal!