Search code examples
springspring-bootcachingguavaspring-cache

Can I use ConcurrentMapCacheFactoryBean to customize the store of Spring Boot simple ConcurrentMapCache?


I am using autoconfigured https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-caching-provider-simple with some @Cacheable and @EnableCaching annotations. I have a couple of named caches. Caching works as expected.

Now I would like to add TTL and maybe some other customizations to my caches. I tried to replace the default ConcurrentHashMap used by ConcurrentMapCacheManager with a Guava cache. I followed suggestion from https://stackoverflow.com/a/8181992/1469083 and did this using ConcurrentMapCacheFactoryBean:

@Bean
public ConcurrentMapCacheFactoryBean cacheFactoryBeanA() {
    ConcurrentMapCacheFactoryBean cacheFactoryBean = new ConcurrentMapCacheFactoryBean();
    cacheFactoryBean.setName("A");
    cacheFactoryBean.setStore(CacheBuilder.newBuilder()
            .expireAfterWrite(DURATION, TimeUnit.SECONDS)
            .build()
            .asMap());
    return cacheFactoryBean;
}

My cache names are A, B or C

@Cacheable("A")

Since I need 3 caches, it looks like I need to configure 3 ConcurrentMapCacheFactoryBeans with different cache names?

@Bean
public ConcurrentMapCacheFactoryBean cacheFactoryBeanA() {
    cacheFactoryBean.setName("A");
    ...
}
@Bean
public ConcurrentMapCacheFactoryBean cacheFactoryBeanB() {
    cacheFactoryBean.setName("B");
    ...
}
@Bean
public ConcurrentMapCacheFactoryBean cacheFactoryBeanC() {
    cacheFactoryBean.setName("C");
    ...
}

is this proper way to do it? Is there easier way to configure ConcurrentMapCache store? This way I lose the automatic cache creation that I had earlier, where I could just add annotation @Cacheable("D") to some method, and cache D would automatically be created.

I know Spring Boot has replaced Guava caching support with autoconfigured Caffeine support, which has properties for TTL, and I could switch to that track also. But I like the flexibility of this approach and also am curious.


Solution

  • I ended up forgetting about ConcurrentMapCacheFactoryBean and customizing CacheManager instead to overwrite ConcurrentMapCacheManager's createConcurrentMapCache(name). This was pretty simple:

    @Configuration
    public class TtlConcurrentMapCacheConfiguration {
    
        public static final int TTL_SECONDS = 60;
    
        @Bean
        public CacheManager cacheManager() {
            return new ConcurrentMapCacheManager() {
                @Override
                protected Cache createConcurrentMapCache(String name) {
                    // storeByValue is false by default (for ConcurrentMapCaches) and wont be true unless we set it
                    // explicitly. I think it is enough to just not support storeByValue, and keep things simple.
                    // https://github.com/spring-projects/spring-framework/issues/18331
                    if (isStoreByValue()) {
                        throw new IllegalStateException("storeByValue is not supported");
                    }
                    ConcurrentMap<Object, Object> store = CacheBuilder.newBuilder()
                            .expireAfterWrite(TTL_SECONDS, TimeUnit.SECONDS)
                            .build()
                            .asMap();
                    return new ConcurrentMapCache(name, store, isAllowNullValues());
                }
            };
        }
    
    }
    

    I don't know where I picked up ConcurrentMapCacheFactoryBean and why having three of those, one for each of my caches, worked like they did in my question. Probably some of my searches found some info that lead me into wrong direction.