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.
The problem is that you are accessing
methodA
directly frommethodB
and therefore this prevents going through the Java proxies which handles the caching mechanims. Additionally, you did not add the@EnableCaching
annotation, so there were actually no caching at all in your test.The following test demonstrates that if you properly go through the proxies created by Spring, the nested cache pattern works as expected: