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:

view sourceprint?1 @Retention(RetentionPolicy.RUNTIME)

2 @Target(ElementType.METHOD)

3 public @interface MemCacheable {

4 /** Defines expiration for some seconds in the future. */

5 int expirationSeconds() default 0;

6 /** Defines whether to cache null values. **/

7 boolean cacheNull() default true;

8 }

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:

view sourceprint?1 @MemCacheable

2 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:

view sourceprint?1 @MemCacheable(expirationSeconds=600)

2 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:

view sourceprint?01 public class CachingInterceptor implements MethodInterceptor {

02 private MemcacheService memcache;

03

04 public CachingInterceptor() {

05 memcache = MemcacheServiceFactory.getMemcacheService();

06 }

07

08 public Object invoke(MethodInvocation invocation) throws Throwable {

09 Method method = invocation.getMethod();

10 MemCacheable memCacheable = method.getAnnotation(MemCacheable.class);

11 if(memCacheable != null) {

12 return handleMemCacheable(invocation, memCacheable);

13 }

14 return invocation.proceed();

15 }

16

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

18 Object key = generateKey(invocation.getThis(), invocation.getMethod(), options.group(), invocation.getArguments());

19 Object result = memcache.get(key);

20 if (result != null)

21 return result;

22 result = invocation.proceed();

23 putToCache(key, result, options);

24 return result;

25 }

26

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

28 try {

29 if (value == null && !options.cacheNull())

30 return false;

31 Expiration expires = null;

32 if (options.expirationSeconds() > 0)

33 expires = Expiration.byDeltaSeconds(options.expirationSeconds());

34 MemcacheService.SetPolicy setPolicy = MemcacheService.SetPolicy.ADD_ONLY_IF_NOT_PRESENT;

35 if (options.setAlways())

36 setPolicy = MemcacheService.SetPolicy.SET_ALWAYS;

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

38 return true;

39 } catch (Throwable t) {

40 return false;

41 }

42 }

43

44 protected Object generateKey(...) {

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

46 }

47 }

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:

view sourceprint?1 public class CachingInterceptorsModule extends AbstractModule {

2 protected void configure() {

3 bindInterceptor(Matchers.any(),

4 Matchers.annotatedWith(MemCacheable.class),

5 new CachingInterceptor());

6 }

7 }

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

view sourceprint?1 public class AOP {

2 private static final Injector injector =

3 Guice.createInjector(new CachingInterceptorsModule());

4

5 public static <T> T getInstance(Class<T> clazz) {

6 return injector.getInstance(clazz);

7 }

8 }

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:

view sourceprint?1 @MemCacheSetter(group="ResultsCache")

2 public void cacheResult(Foo param1, Bar param2, Result result)

3 {}

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:

view sourceprint?1 @MemCacheable(group="ResultsCache")

2 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.


文章不錯,轉至http://radomirml.com/2010/05/20/declarative-caching-for-appengine
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章