Monday, June 7, 2010

Spring, AspectJ, Ehcache Method Caching Aspect

I know this is not a new concept, and I am pretty sure there a couple implementations of method caching out there, possibly some much more thorough, but I feel quite strongly about it and I think that it is still one of the most beneficial things ever to come out of the AOP design philosophy. It is also something that if implemented correctly can vastly improve the performance for a lot of systems out there.

A good couple years back, I stumbled across a blog post describing the concept of a method caching interceptor, that would allow you to cache method calls without changing code and thereby make some dramatic performance improvements on existing systems. I jumped on the concept, took some code from the blog, fixed an issue or 2, changed a couple things to my requirements, demo'ed it to the system architect at the time and implemented it for the next release cycle. That bit of code has been running for about 4 years now and had a huge impact on our performance and the pressures our legacy system placed on other internal teams.

Due to the joys that are large corporate organisations, the use of the latest version of Spring and especially AspectJ are not allowed by the standards and redtape, but that allows me to write a whole new neat version without infringing on too many IP issues :) and publish it here.

Full source code, maven pom and eclipse files are available for download:
Source Code
or through SVN:
MethodCacheAspect trunk

In the project there are 3 main java files of interest:
MethodCacheAspect.java
CacheKeyStrategy.java
DefaultCacheKeyStrategy.java

The CacheKeyStrategy interface came about when I noticed that even though objects were "equal" there were 2 separate items in ehcache. This was because of 2 issues actually, order of items and just using .toString() to build up a key for ehcache. Not wanting to enforce the order or method of generating a key, I created a the interface and a default implementation. If future users want to handle it any different all they need to do is implement it and specify the implementation in Spring. I have 2 custom implementations in my live project, but for this blog posting I have just created a simple default.

note:I removed all JavaDoc just to save space, the downloadable code has docs.

package javaitzen.spring.interceptors;

public interface CacheKeyStrategy {

    String generateKey() throws IllegalStateException;

    String classForStrategy();

    void setObject(final Object object);
}


Below is the simple default key generator, just for convenience, that does a Collections.sort and uses the objects' hashcode method.

package javaitzen.spring.interceptors;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.aspectj.lang.ProceedingJoinPoint;

public class DefaultCacheKeyStrategy implements CacheKeyStrategy {

    private Logger logger = Logger.getLogger(DefaultCacheKeyStrategy.class.getName());
    private ProceedingJoinPoint pjp;

    public DefaultCacheKeyStrategy() {
    }

    public DefaultCacheKeyStrategy(final ProceedingJoinPoint pjp) {
        this.pjp = pjp;
    }

    public String generateKey() throws IllegalStateException {

        String targetName = pjp.getSignature().getDeclaringTypeName();
        String methodName = pjp.getSignature().getName();
        Object[] arguments = pjp.getArgs();

        return getCacheKey(targetName, methodName, arguments);
    }

    private String getCacheKey(final String targetName, final String methodName, final Object[] arguments) {
        StringBuilder sb = new StringBuilder();
        sb.append(targetName).append(".").append(methodName);

        for (Object arg : arguments) {
            if (arg instanceof Map< ?, ? >) {
                sb.append(expandMap((Map) arg));
            } else if (arg instanceof Collection< ? >) {
                sb.append(buildBuffer((Collection< ? >) arg));
            } else {
                sb.append(".").append(arg.hashCode());
            }
        }

        logger.log(Level.INFO, "Cache key is: [" + sb.toString() + "]");
        return sb.toString();
    }

    private StringBuilder buildBuffer(final Collection< ? > values) {
        StringBuilder bob = new StringBuilder();
        Collections.sort((List< ? >) values, new Comparator< Object >() {
            public int compare(final Object arg0, final Object arg1) {
                String thing1 = arg0.toString();
                String thing2 = arg1.toString();
                return thing1.compareTo(thing2);
            }
        });
        for(Object o : values){
            if (o != null) {
                bob.append(".").append(o.hashCode());
            }            
        }
        return bob;
    }

    private StringBuilder expandMap(final Map< ?, ? > args) {
        if (args == null) {
            return new StringBuilder();
        }

        List< Object > values = new ArrayList< Object >();
        for (Entry< ?, ? > entry : args.entrySet()) {
            values.add(entry.hashCode());
        }
        return buildBuffer(values);
    }

   public void setObject(final Object invocation) {
        this.pjp = (ProceedingJoinPoint) invocation;
    }

    public String classForStrategy() {
        return "*";
    }
}


Now for the juicy bit, the aspect checks if there are any cache key strategies compares types with information from the ProceedingJoinPoint, it extracts the relevant information and generates a key. It then checks if there is value for that key in the cache and returns that value if relevant, else it executes the method with "pjp.proceed()" and caches the result, so that for the next time the same method is called with the same parameters it can just be returned from the cache. I cant remember the exact numbers but with implementing this between our legacy system and another team we cut our daily calls to that teams web service from something like 65000 to around 6000.

package javaitzen.spring.interceptors;

import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

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

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class MethodCacheAspect {

    private Logger logger = Logger.getLogger(MethodCacheAspect.class.getName());
    private Cache cache;
    private CacheKeyStrategy defaultKeyStrat;
    private List< CacheKeyStrategy > keyStrategies = new LinkedList< CacheKeyStrategy >();

    public void setCacheKeyStrategies(final List< CacheKeyStrategy > cacheKeys) {
        this.keyStrategies = cacheKeys;
    }

    public void addCacheKeyStrategy(final CacheKeyStrategy cacheKey) {
        this.keyStrategies.add(cacheKey);
    }

    public Object aroundAdvice(final ProceedingJoinPoint pjp) throws Throwable {

        Object[] arguments = pjp.getArgs();
        Object result;
        StringBuilder cacheKey = new StringBuilder();
        defaultKeyStrat = new DefaultCacheKeyStrategy(pjp);

         if (!keyStrategies.isEmpty()) {
            logger.log(Level.INFO, "Have a Key Strategy to use...");
            for (CacheKeyStrategy strat : keyStrategies) {
                if ((arguments != null) && (arguments.length != 0)) {
                    logger.log(Level.INFO, "Have Arguments...");
                    for (Object arg : arguments) {
                        if (Class.forName(strat.classForStrategy()).isInstance(arg)) {
                            strat.setObject(arg);
                            logger.log(Level.INFO, "Using Strategy...");
                            cacheKey.append(strat.generateKey());
                        }
                    }
                }
            }
        }

        if (cacheKey.length() == 0) {
            logger.log(Level.INFO, "Using Default...");
            cacheKey.append(defaultKeyStrat.generateKey());
        }
        Element element = cache.get(cacheKey.toString());

        // not in cache
        if (element == null) {
            result = pjp.proceed();
            if (result != null && !(result instanceof Serializable)) {
                throw new RuntimeException("[" + result.getClass().getName() + "] is not Serializable");
            }
            logger.log(Level.INFO, ">>> caching result - " + cacheKey);
            element = new Element(cacheKey.toString(), (Serializable) result);
            cache.put(element);
        } else {
            logger.log(Level.INFO, ">>> returning result from cache");
            return element.getValue();
        }

        return result;
    }

    public Cache getCache() {
        return cache;
    }

    public void setCache(final Cache cache) {
        this.cache = cache;
    }

}


In the application context below you will notice execution(* theBusinessMethod(..)). That only attaches the aspect for theBusinessMethod, in my test class in the downloadable source.

Just for quick reference here are a couple more examples from the Spring docs:
The execution of any public method:
execution(public * *(..))
The execution of any method with a name beginning with "set":
execution(* set*(..))
The execution of any method defined by the AccountService interface:
execution(* com.xyz.service.AccountService.*(..))
The execution of any method defined in the service package:
execution(* com.xyz.service.*.*(..))
The execution of any method defined in the service package or a sub-package:
execution(* com.xyz.service..*.*(..))

Application Context:



 

 
  
   
  
 

 
  
   
   
  
 

 
 
  
   classpath:javaitzen/spring/interceptors/ehcache.xml
   
  
  
 

 
 
  
   
  
  
   testCache
  
 





And finally the ehcache.xml:

 
 

 


4 comments:

  1. hello,
    and what about this solution : http://www.jeviathon.com/2010/04/caching-java-methods-with-spring-3.html ?
    regards

    ReplyDelete
  2. Hi,
    As I mentioned, this is not a new concept, and there will be a whole bunch of different solutions \ implementations.

    As for using annotations, I quite like it, although personally, I prefer not changing any code when implementing AOP. I like the idea that it can be done purely through configuration.

    The above statement is bound to start a debate about what is metadata, config and where the line between config and code ends as both require a recompile.

    But actually the most likely reason I didn't go the annotation route was the original code was written in 1.4 :) (it is that old)

    ReplyDelete
  3. Hi Brain

    I tried your sample it looks like 5 ms is not enough to pass the test maybe better make it 50 ms. But test is ok it does not claculate next time method called :)

    ReplyDelete

Popular Posts

Followers