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

1

15

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?

Hixson answered 21/5, 2019 at 13:14 Comment(4)
This was how the Guava support worked and only minor changes were made for migrating to Caffeine. There is an interesting approach if you use a custom adapter.Gladis
Hi Ben, pleasure to see you here :) I've read through the issue and it seems your view on this is that we should be using the provider directly. The poster's solution is quite clever and I'll look into it further. I have a follow-up question - using your approach, where would you say is the best practice to place the cache definition itself? In the class where it's used? In a configuration class which makes it available for injection? Somewhere else?Hixson
I use Guice at work, which is much less AOP minded and plain old Java. I’ll define the spec in a configuration file, parse into and inject a CaffeineSpec, build in the class with a private method as the loader, and register it into the metrics reporter. I might use map computations or asynchronous caching, etc. Other times I define it in the class when not configurable, like a weak keyed cache for an internal lifecycle. I don’t find using a library directly feels dirty and an AOP abstraction appears limiting or not helpful for my uses. But I haven’t used Spring since 1.x so my habits differ.Gladis
I know this is an old thread but I implemented this behavior without a LoadingCache. I used Springs @Scheduled(fixedDelay = 1000, intitialDelay = 0) and @CachePut annotations on method that returns the updated value of an expensive call (in conjunction with another method that uses standard @Cacheable). Of course it only works if you don't need a specific cache key (like caching a big collection of data)Calyces
S
1

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.

Summarize answered 16/3, 2023 at 21:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.