Search code examples
javaspringcachingguavaehcache

Spring @Cacheable: Preserve old value on error


I am planning to use the Spring @Cacheable annotation in order to cache the results of invoked methods.

But this implementation somehow does not look very "safe" to me. As far as I understand, the returned value will be cached by the underlying caching engine and will be deleted when the Spring evict method is called.

I would need an implementation which does not destroy the old value until the new value was loaded. This would be required and the following scenario should work:

  1. Cacheable method is called -> Valid result returned
  2. Result will be cached by the Spring @Cacheable backend
  3. Spring invalidates cache because it expired (e.g. TTL of 1 hour)
  4. Cacheable method is called again -> Exception/null value returned!
  5. OLD result will be cached again and thus, future invokations of the method will return a valid result

How would this be possible?


Solution

  • Your requirement of serving old values if the @Cacheable method throws an exception can easily be achieved with a minimal extension to Google Guava.

    Use the following example configuration

    @Configuration
    @EnableWebMvc
    @EnableCaching
    @ComponentScan("com.yonosoft.poc.cache")
    public class ApplicationConfig extends CachingConfigurerSupport {
        @Bean
        @Override
        public CacheManager cacheManager() {
            SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
    
            GuavaCache todoCache = new GuavaCache("todo", CacheBuilder.newBuilder()
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .maximumSize(10)
                .build(new CacheLoader<Object, Object>() {
                    @Override
                    public Object load(Object key) throws Exception {
                        CacheKey cacheKey = (CacheKey)key;
                        return cacheKey.method.invoke(cacheKey.target, cacheKey.params);
                    }
                }));
    
            simpleCacheManager.setCaches(Arrays.asList(todoCache));
    
            return simpleCacheManager;
        }
    
        @Bean
        @Override
        public KeyGenerator keyGenerator() {
            return new KeyGenerator() {
                @Override
                public Object generate(Object target, Method method, Object... params) {
                    return new CacheKey(target, method, params);
                }
            };
        }
    
        private class CacheKey extends SimpleKey {
            private static final long serialVersionUID = -1013132832917334168L;
            private Object target;
            private Method method;
            private Object[] params;
    
            private CacheKey(Object target, Method method, Object... params) {
                super(params);
                this.target = target;
                this.method = method;
                this.params = params;
            }
        }
    }
    

    CacheKey serves the single purpose of exposing SimpleKey attributes. Guavas refreshAfterWrite will configure the refresh time without expiring the cache entries. If the methods annotated with @Cacheable throws an exception the cache will continue to serve the old value until evicted due to maximumSize or replaced by a new value from succesful method response. You can use refreshAfterWrite in conjunction with expireAfterAccess and expireAfterAccess.