Declarative Caching for AppEngine

I really hate when my source files are bloated with lines and lines of code that do not implement any business logic at all. One of the sources of this was certainly code related to caching. Using Google’s Memcache service with the low-level API or via JCache facade is not complicated but it really was not elegant enough for my taste. I was hoping to find some annotations-based solution that will work for me, but no luck.

So, this appeared as a challenge and, inspired by the Springmodules caching implementation, I decided to research further and see how complicated could it be to implement. On the other hand, I just waited for an excuse to write my first annotation library. :-)

(Note: If I were using Spring, I would probably try to use Springmodules. However, I’ve decided earlier not to use Spring on my GAE project and took a more lightweight path with Stripes.)

Annotation class

I started defining an annotation class that would allow me to tag methods for which I wanted to cache return value:

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MemCacheable {
  /** Defines expiration for some seconds in the future. */
  int expirationSeconds() default 0;
  /** Defines whether to cache null values. **/
  boolean cacheNull() default true;
}

The idea was that @MemCacheable on a method would mean that return value of the method should be cached (in Memcache) as long as possible, automatically generating a caching key from the method signature and its arguments. For example:

1
2
@MemCacheable
public Result myServiceMethod(Foo arg1, Bar arg2)

However, you could easily override this specifying how long do you want the keep the return value in cache:

1
2
@MemCacheable(expirationSeconds=600)
public Result myServiceMethod(Foo param1, Bar param2)

OK, creation of the annotation class was easy but it has no real value without a logic behind it.

Method interception

Next, I needed a solution for a method interception that will allow me to implement around advice for tagged methods. Until that moment, my project didn’t use any dependency injection framework so I decided to use Guice because:

  • it doesn’t force me to change to many things in the project (except few factory classes), and

  • it uses standard AOPAlliance interfaces for advices so, at least theoretically, interceptors could be reused with some other framework too.

Here’s the simplified version of the method interceptor:

[sourcecode language=”java” wraplines=”false”] public class CachingInterceptor implements MethodInterceptor { private MemcacheService memcache;

public CachingInterceptor() {

memcache = MemcacheServiceFactory.getMemcacheService();

}

public Object invoke(MethodInvocation invocation) throws Throwable {

Method method = invocation.getMethod();
MemCacheable memCacheable = method.getAnnotation(MemCacheable.class);
if(memCacheable != null) {
  return handleMemCacheable(invocation, memCacheable);
}
return invocation.proceed();

}

private Object handleMemCacheable(MethodInvocation invocation, MemCacheable options) throws Throwable {

Object key = generateKey(invocation.getThis(), invocation.getMethod(), options.group(), invocation.getArguments());
Object result = memcache.get(key);
if (result != null)
  return result;
result = invocation.proceed();
putToCache(key, result, options);
return result;

}

protected boolean putToCache(Object key, Object value, MemCacheable options) {

try {
  if (value == null && !options.cacheNull())
    return false;
  Expiration expires = null;
  if (options.expirationSeconds() > 0)
    expires = Expiration.byDeltaSeconds(options.expirationSeconds());
  MemcacheService.SetPolicy setPolicy = MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT;
  if (options.setAlways())
    setPolicy = MemcacheService.SetPolicy.SET_ALWAYS;
  memcache.put(key, value, expires, setPolicy);
  return true;
} catch (Throwable t) {
  return false;
}

}

protected Object generateKey(…) {

// generate key using hash codes of the target method, method arguments, etc.

} } [/sourcecode]

For the key generation I used code from the Springmodules project mentioned above.

Gluing it with Guice

Adding Guice to the project was simple. First, I defined a module that ties caching interceptor with the annotation class:

[sourcecode language=”java”] public class CachingInterceptorsModule extends AbstractModule { protected void configure() {

bindInterceptor(Matchers.any(),
  Matchers.annotatedWith(MemCacheable.class),
  new CachingInterceptor());

} } [/sourcecode]

That’s about it. To instantiate classes that use the caching annotations I created a simple class with the Guice injector:

[sourcecode language=”java”] public class AOP { private static final Injector injector =

Guice.createInjector(new CachingInterceptorsModule());

public static T getInstance(Class clazz) {

return injector.getInstance(clazz);

} } [/sourcecode]

Setter annotation (bonus)

After the initial implementation was in place, my appetites were growing… What if I wanted to update the cached value before it expires? I could use something like this:

1
2
3
@MemCacheSetter(group="ResultsCache")
public void cacheResult(Foo param1, Bar param2, Result result)
{}

With the above annotation, I could generate a cache key using all method arguments except the last one, which is the object we’re storing to cache. So, for the above example, we generate a cache key using the group name specified in the annotation and two method arguments (param1 and param2). Simple isn’t it? (We don’t even need the method body as the interceptor will never invoke it.)

This requires small change to the @MemCacheable annotation. To benefit from the setter annotation, we’ll add support for the group argument:

1
2
@MemCacheable(group="ResultsCache")
public Result myServiceMethod(Foo  arg1, Bar arg2)

In effect, when the group name is specified, it’s used as a part of a cache key instead of the method signature. In cases when the group name is not provided, the method signature will be used instead.

I pushed the complete project sources and jar to the GJUtil project so you can find it there if you’re interested. The code is stable and already running in my BugDigger application. (If you’re a web developer, and I guess you are if you’re reading this, you may find it useful too.)

A downside of this library is its dependency on Guice for AOP things. I think it would be useful to remove dependency on any framework and inject caching interceptors with bytecode manipulation (e.g. using ASM) as a part of the compilation process. This would enable use of caching annotations with any class, not just those instantiated by dependency injector. If anyone is interested in sponsoring this effort (or maybe contributing some code), let me know.

The next week I’ll extend to the above code and show you how to use the same technique for caching in HTTP request or application context, avoiding RPC calls for the Memcache when local or request scope caching is sufficient.

Update: part 2

public class CachingInterceptor implements MethodInterceptor {private static final Logger log = Logger.getLogger(CachingInterceptor.class.getName());private MemcacheService memcache;public CachingInterceptor() { memcache = MemcacheServiceFactory.getMemcacheService(); memcache.setNamespace(“GAE Caching Interceptor”);

// added handler because I had some strange errors (“entity too large” although it was not) // with dev server but not seen since memcache.setErrorHandler(new LogAndContinueErrorHandler(Level.FINE) {

@Override public void handleServiceError(MemcacheServiceException thrown) {

log.log(Level.SEVERE, “Memcache service error!”, thrown);

super.handleServiceError(thrown); } }); }

public Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod();

MemCacheable memCacheable = method.getAnnotation(MemCacheable.class); if(memCacheable != null) { return handleMemCacheable(invocation, memCacheable); } MemCachedSetter setter = method.getAnnotation(MemCachedSetter.class); if(setter != null) { return handleMemCachedSetter(invocation, setter); } return invocation.proceed(); }

private Object handleMemCacheable(MethodInvocation invocation, MemCacheable options) throws Throwable { Object key = generateKey(invocation.getThis(), invocation.getMethod(), options.group(), invocation.getArguments()); Object result = getFromCache(key); if (result != null) return result; result = invocation.proceed(); putToCache(key, result, options); return result; }

private Object handleMemCachedSetter(MethodInvocation invocation, MemCachedSetter options) throws Throwable { Object[] args = invocation.getArguments(); if(args.length < 2) { throw new IllegalArgumentException(“MemCachedSetter annotation requires at least 2 arguments on method ” + invocation.getMethod()); } Object key = generateKey(invocation.getThis(), null, options.group(), args, 0, args.length - 1); // the last argument is an object we’re storing to cache Object value = args[args.length - 1];

if(putToCache(key, value, options)) { if(log.isLoggable(Level.FINE)) log.fine(“Stored object to cache group ” + options.group()); }

// do we have a use case for invoking the original method? return null; }

protected Object generateKey(Object target, Method method, String key, Object[] methodArguments) { return generateKey(target, method, key, methodArguments, 0, methodArguments != null ? methodArguments.length : 0); }

protected Object generateKey(Object target, Method method, String group, Object[] methodArguments, int beginIndex, int endIndex) { HashCodeCalculator hashCodeCalculator = new HashCodeCalculator();

if(group != null && group.length() > 0) hashCodeCalculator.append(group.hashCode()); else if(method != null) hashCodeCalculator.append(System.identityHashCode(method));

if (methodArguments != null) { for (int i = beginIndex; i < endIndex; i++) { Object methodArgument = methodArguments[i]; int hash = 0; // if (generateArgumentHashCode) { // hash = Reflections.reflectionHashCode(methodArgument); // } else { hash = Objects.nullSafeHashCode(methodArgument); // } hashCodeCalculator.append(hash); } }

long checkSum = hashCodeCalculator.getCheckSum(); int hashCode = hashCodeCalculator.getHashCode();

Object cacheKey = new HashCodeCacheKey(checkSum, hashCode); return cacheKey; }

protected Object getFromCache(Object key) { try { Object value = memcache.get(key); if(log.isLoggable(Level.FINE)) { if(value != null) log.fine(“Found in cache key ” + key); else log.fine(“Not found in cache key ” + key); } return value; } catch (Throwable t) { log.log(Level.WARNING, “Error retrieving object from cache.”, t); return null; } }

protected boolean putToCache(Object key, Object value, MemCacheable options) { try { if (value == null && !options.cacheNull()) return false;

Expiration expires = null; if (options.expirationSeconds() > 0) expires = Expiration.byDeltaSeconds(options.expirationSeconds());

MemcacheService.SetPolicy setPolicy = MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT; if (options.setAlways()) setPolicy = MemcacheService.SetPolicy.SET_ALWAYS;

memcache.put(key, value, expires, setPolicy);

if(log.isLoggable(Level.FINE)) { log.fine(“Stored to cache key ” + key); } return true; } catch (Throwable t) { log.log(Level.WARNING, “Error storing object to cache.”, t); return false; } }

protected boolean putToCache(Object key, Object value, MemCachedSetter options) { try { if (value == null && !options.cacheNull()) return false;

Expiration expires = null; if (options.expirationSeconds() > 0) expires = Expiration.byDeltaSeconds(options.expirationSeconds());

MemcacheService.SetPolicy setPolicy = MemcacheService.SetPolicy.SET_ALWAYS;

memcache.put(key, value, expires, setPolicy);

if(log.isLoggable(Level.FINE)) { log.fine(“Stored to cache key ” + key + ” to group ” + options.group()); } return true;

} catch (Throwable t) { log.log(Level.WARNING, “Error storing object to cache.”, t); return false; } }

}

Comments