Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to adapt a Caffeine LoadingCache for use with Spring Boot's @Cacheable?

I am working on a large Spring Boot 2.1.5.RELEASE application, using Caffeine as a cache provider. To prevent I/O bottlenecks, I am using a Caffeine LoadingCache<K,V> in (essentially) the following way:

LoadingCache<K, V> cache = Caffeine.newBuilder()
    .refreshAfterWrite(1, TimeUnit.MINUTES)
    .build(loadStuffOverHttp());

As far as I know, I cannot use refresh-after-write functionality without using LoadingCache.

However, LoadingCache does not implement Spring's Cache. This means I cannot rely on @Bean methods to register my caches, which each need to be configured differently. Being registered in the Spring context would, in theory, allow them to be used in conjunction with Spring's @Cacheable annotation.

From what I can see from the source code for CaffeineCacheConfiguration.java, I cannot rely on Spring Boot's auto-configuration either. Beans of type CaffeineCache (Spring's cache adapter pattern for Caffeine's Cache<K,V>) are registered automatically, but the adapter forces me to use <Object, Object> as the generic types of my CacheLoader<K, V>. I only want to do this as a last resort.

This SO question shows it is possible to configure different caches programmatically:

Just expose your custom caches as beans. They are automatically added to the CaffeineCacheManager.

However, doing this with LoadingCache<K, V> (with arbitrary K, V, not <Object, Object>) seems to be harder.

This SO question seems to indicate doing it with a SimpleCacheManager instead of a CaffeineCacheManager is possible - but using this solution requires the CacheLoader definition to be available to the Cache bean. This may easily require the injection of the service using the cache via @Cacheable in the first place, for example in the case of an expensive HTTP call. It also seems like a solution prone to dependency cycles, but please correct me if this is not the case.

Question

What is the proper way to define a Caffeine LoadingCache<K, V> for use with Spring's @Cacheable?

like image 270
Paul Benn Avatar asked Jan 20 '26 03:01

Paul Benn


1 Answers

To define a Caffeine LoadingCache<K, V> for use with Spring's @Cacheable annotation, you can create a custom implementation of Cache that wraps a Caffeine LoadingCache. Here's an example implementation:

import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;
import java.util.concurrent.Callable;
import com.github.benmanes.caffeine.cache.LoadingCache;

public class CaffeineCache implements Cache {

    private final String name;
    private final LoadingCache<Object, Object> cache;

    public CaffeineCache(String name, LoadingCache<Object, Object> cache) {
        this.name = name;
        this.cache = cache;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Object getNativeCache() {
        return cache;
    }

    @Override
    public ValueWrapper get(Object key) {
        Object value = cache.getIfPresent(key);
        return (value != null ? new SimpleValueWrapper(value) : null);
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        Object value = cache.getIfPresent(key);
        if (value != null && type != null && !type.isInstance(value)) {
            throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
        }
        return (T) value;
    }

    @Override
    public void put(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public void evict(Object key) {
        cache.invalidate(key);
    }

    @Override
    public void clear() {
        cache.invalidateAll();
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        try {
            Object value = cache.get(key, k -> valueLoader.call());
            if (value != null && !value.getClass().isAssignableFrom(valueLoader.getClass())) {
                throw new IllegalStateException("Cached value is not of required type [" + valueLoader.getClass().getName() + "]: " + value);
            }
            return (T) value;
        } catch (Exception ex) {
            throw new ValueRetrievalException(key, valueLoader, ex);
        }
    }
}

Then you can register your cache as a Spring bean and use it with the @Cacheable annotation like this:

@Bean
public Cache myCache() {
    LoadingCache<MyKey, MyValue> cache = Caffeine.newBuilder()
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        .build(loadStuffOverHttp());
    return new CaffeineCache("myCache", cache);
}

@Cacheable("myCache")
public MyValue getValue(MyKey key) {
    ...
}

This approach allows you to define and configure your Caffeine cache as a LoadingCache<K, V> while still being able to use it with Spring's caching abstraction.

like image 147
Laurent Avatar answered Jan 21 '26 15:01

Laurent