class SampleRepositoryXmlImpl implements SampleRepository {
public SetgetSamples(int n) {
Setsamples = this.getSamplesFromDisk();
return getUpToNRandomSamplesFromSet(n, samples);
}
@Cacheable(groups="someCacheableGroup")
public SetgetSamplesFromDisk() {
...
}
}
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.