Wednesday, June 11, 2008

Spring cache module catcha!

Caching using JDK 5.0 annotation with Spring seems to be easy. It has a catcha though, when you use the @Cacheable annotation in the second-level call of the object you want to cache. Let me explain it with an example. Consider this class:


class SampleRepositoryXmlImpl implements SampleRepository {
public Set getSamples(int n) {
Set samples = this.getSamplesFromDisk();
return getUpToNRandomSamplesFromSet(n, samples);
}

@Cacheable(groups="someCacheableGroup")
public Set getSamplesFromDisk() {
...
}
}


See, the innocent call to getSamplesFromDisk() from getSamples() should get invoked only when the return value expires in the cache. But let's see how Spring's JdkDynamicAopProxy works (excerpt from Spring's source code JdkDynamicAopProxy:189):


// Get the interception chain for this method.
List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// Check whether we have any advice. If we don't, we can fallback on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// We can skip creating a MethodInvocation: just invoke the target directly
// Note that the final invoker must be an InvokerInterceptor so we know it does
// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args);
} else {
// We need to create a method invocation...
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}


The "if (chain.isEmpty)" condition block evaluates to true when getSamples() is invoked but then in the inner call to getSamplesFromDisk(), the same condition block evaluates to false since it determines that there is no more chain, thus invokes the method directly, ignoring the interceptor created by @Cacheable annotation!

For now, to avoid the problem, you should only tag methods as @Cacheable only in the most external level call of the object. If necessary, refactor your code and extract the method to a Strategy object.

No comments: