I was given the task to make use of SpringCache for one of our services to reduce the number of DB lookups. While testing the implementation I noticed that some of the cacheable operations are invoked multiple times via log-statements. Investigations revealed that if a cacheable operation is called within a cachable method, the nested operation is not cached at all. Therefore, a later invocation of the nested operation leads to a further lookup.
A simple unit-test describing the problem is enlisted below:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringCacheTest.Config.class} )
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class SpringCacheTest {
private final static String CACHE_NAME = "testCache";
private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final static AtomicInteger methodInvocations = new AtomicInteger(0);
public interface ICacheableService {
String methodA(int length);
String methodB(String name);
}
@Resource
private ICacheableService cache;
@Test
public void testNestedCaching() {
String name = "test";
cache.methodB(name);
assertThat(methodInvocations.get(), is(equalTo(2)));
cache.methodA(name.length());
// should only be 2 as methodA for this length was already invoked before
assertThat(methodInvocations.get(), is(equalTo(3)));
}
@Configuration
public static class Config {
@Bean
public CacheManager getCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(CACHE_NAME)));
return cacheManager;
}
@Bean
public ICacheableService getMockedEntityService() {
return new ICacheableService() {
private final Random random = new Random();
@Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
public String methodA(int length) {
methodInvocations.incrementAndGet();
LOG.debug("Invoking methodA");
char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i=0; i<length; i++) {
sb.append(chars[random.nextInt(chars.length)]);
}
String result = sb.toString();
LOG.debug("Returning {} for length: {}", result, length);
return result;
}
@Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
public String methodB(String name) {
methodInvocations.incrementAndGet();
LOG.debug("Invoking methodB");
String rand = methodA(name.length());
String result = name+"_"+rand;
LOG.debug("Returning {} for name: {}", result, name);
return result;
}
};
}
}
}
The actual work of both methods is unimportant for the test-case itself as just the caching should be tested.
I somehow understand the reason why the result of the nested operation is not cached, but I was wondering if there is a configuration available, which I haven't figured out yet, to enable caching for return values of nested cacheable operations.
I know that through refactoring and providing the return value from the nested operation as argument for the outer operation will work, but as this might involve to change a number of operations (as well as unit-test them) a configuration or other workaround (if available) would be preferable in our concrete case.