4
votes

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?

2

2 Answers

2
votes

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.

0
votes

I may be wrong in my reading of the Spring code, notably org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts), but I believe the abstraction does not provide what you ask indeed.

  1. Spring will not expire entries, this will be left to the underlying caching implementation.
  2. You mention that you would like to see values even though they are expired. That's against the expiry abstraction used in most cache implementations that I know of.
  3. Returning a previously cached value on invocation error is clearly use case specific. The Spring abstraction will simply throw the error back at the user. The CacheErrorHandler mechanism only deals with cache invocation related exceptions.

All in all, it seems to me that what you are asking for is very use case specific and thus not something an abstraction would/should offer.