Multiple Caffeine LoadingCaches added to Spring CaffeineCacheManager
Asked Answered
W

4

16

I'm looking to add several distinct LoadingCache's to a Spring CacheManager, however I don't see how this is possible using CaffeineCacheManager. It appears that only a single loader is possible for refreshing content, however I need separate loaders for each cache. Is it possible to add multiple loading caches to a Spring cache manager? If so, then how?

CaffeineCacheManager cacheManage = new CaffeineCacheManager();

LoadingCache<String, Optional<Edition>> loadingCache1 = 
            Caffeine.newBuilder()
            .maximumSize(150)
            .refreshAfterWrite(5, TimeUnit.MINUTES)
            .build(test -> this.testRepo.find(test));

LoadingCache<String, Optional<Edition>> loadingCache2 = 
            Caffeine.newBuilder()
            .maximumSize(150)
            .refreshAfterWrite(5, TimeUnit.MINUTES)
            .build(test2 -> this.testRepo.find2(test2));

// How do I add to cache manager, and specify a name?
Whereto answered 12/6, 2017 at 19:27 Comment(4)
I think you can have multiple cache managers, so perhaps one per? It may b e cleaner code to use the native APIs rather than Spring's abstraction.Latium
Ben, can we associate a loader per Caffeine cache in the first place? If so, Steve could only create CaffeineCache beans with the generic cache manager. That's configuration so it's ok to use the native APIs and the Spring's abstraction has nothing to do with that IMOSheffy
@BenManes That was my first thought. A separate cache manager per cache, but just seemed a bit overkill, so I am currently just bypassing Spring's caching abstraction. I do lose out on some of the niceties that come along with it, like actuator/stats integration, though I suppose that could be integrated easily manually.Whereto
@StephaneNicoll Yes, but as the final step in the building phase to determine which interface to return (manual, loading, async loading) as shown above. Does Spring require the builder or can caches be given directly?Latium
S
31

Yes it is possible. Since you need to fine tune every cache, you are probably better at defining them yourself. Back to your example, the next step would be:

SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
    new CaffeineCache("first", loadingCache1),
    new CaffeineCache("second", loadingCache2)));

And then you can use that as usual, e.g.

@Cacheable("first")
public Foo load(String id) { ... }

If you are using Spring Boot, you can just expose the individual cache as beans (so org.springframework.cache.Cache implementations) and we'll detect them and create a SimpleCacheManager automatically for you.

Note that this strategy allows you to use the cache abstraction with different implementations. first could be a caffeine cache and second a cache from another provider.

Sheffy answered 15/6, 2017 at 7:46 Comment(4)
Can you please provide example of exposing LoadingCache as an org.springframework.cache.Cache bean?Haarlem
How can I autowire the CacheManager if each cache is exposed as a bean in the Configuration class ? When is the CacheManger instance created ? How do I autowire it from the application context if i am trying to test it outside of a Springboot appBison
SimpleCacheManager automatically for you?Middling
create your caches as beans and they'll be wrapped automatically in a cache managerSheffy
E
3

Having this class will allow you to use @Cacheable("cacheA") where you want as normal:

@EnableCaching
@Configuration
public class CacheConfiguration {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.registerCustomCache("cacheA", defaultCache());
        manager.registerCustomCache("cacheB", bigCache());
        manager.registerCustomCache("cacheC", longCache());
        // to avoid dynamic caches and be sure each name is assigned to a specific config (dynamic = false)
        // throws error when tries to use a new cache
        manager.setCacheNames(Collections.emptyList());
        return manager;
    }

    private static Cache<Object, Object> defaultCache() {
        return Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }

    private static Cache<Object, Object> bigCache() {
        return Caffeine.newBuilder()
                .maximumSize(5000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }

    private static Cache<Object, Object> longCache() {
        return Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(1, TimeUnit.HOURS)
                .build();
    }
}
Emphasis answered 27/7, 2021 at 13:50 Comment(0)
P
1

Thanks for @rado, this is improved version of his answer. This way we can configure the cache from application properties directly

cache:
  specs:
    big-cache:
      expire-after: WRITE
      timeout: 2h
      max-size: 1000
    long-cache:
      expire-after: ACCESS
      timeout: 30d
      max-size: 100

We need a cache properties for this

@Data
@EnableConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "cache")
public class CacheProperties {

    private static final int DEFAULT_CACHE_SIZE = 100;

    private Map<String, CacheSpec> specs = new HashMap<>();

    @Data
    public static class  CacheSpec {
        private Duration timeout;
        private Integer maxSize = DEFAULT_CACHE_SIZE;
        private ExpireAfter expireAfter = ExpireAfter.WRITE;
    }

    enum ExpireAfter { WRITE, ACCESS }
}

And then we can configure directly from external config file

@EnableCaching
@Configuration
@RequiredArgsConstructor
public class CacheConfiguration {

    private final CacheProperties cacheProperties;

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();

        Map<String, CacheProperties.CacheSpec> specs = cacheProperties.getSpecs();
        specs.keySet().forEach(cacheName -> {
            CacheProperties.CacheSpec spec = specs.get(cacheName);
            manager.registerCustomCache(cacheName, buildCache(spec));
        });

        // to avoid dynamic caches and be sure each name is assigned
        // throws error when tries to use a new cache
        manager.setCacheNames(Collections.emptyList());
        return manager;
    }

    private Cache<Object, Object> buildCache(CacheProperties.CacheSpec cacheSpec) {
        if (cacheSpec.getExpireAfter() == CacheProperties.ExpireAfter.ACCESS) {
            return Caffeine.newBuilder()
                    .expireAfterAccess(cacheSpec.getTimeout())
                    .build();
        }
        return Caffeine.newBuilder()
                .expireAfterWrite(cacheSpec.getTimeout())
                .build();
    }
}

Now you can use the cache with using cache name

    @Cacheable(cacheNames = "big-cache", key = "{#key}", unless="#result == null")
    public Object findByKeyFromBigCache(String key) {
        // create the required object and return
    }

    @Cacheable(cacheNames = "long-cache", key = "{#key}", unless="#result == null")
    public Object findByKeyFromLongCache(String key) {
        // create the required object and return
    }
Pissarro answered 22/1, 2023 at 13:55 Comment(0)
B
1

It's mandatory to build your custom Caffeine Cache using a com.github.benmanes.caffeine.cache.Ticker.

This is a working example tested with Java 17, Spring Boot 2.7.7 and Caffeine 3.1.6 where we configure a cacheOne with an expiration time of 60 seconds and a cacheTwo which expires after one hour or 3600 seconds:

@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManagerTicker(Ticker ticker) {

        var cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(List.of(
                this.buildCache("cacheOne", ticker, 1, 60, TimeUnit.SECONDS),
                this.buildCache("cacheTwo", ticker, 1, 3600, TimeUnit.SECONDS)
        ));

        return cacheManager;
    }

    private CaffeineCache buildCache(String cacheName, Ticker ticker,
                                     int maxSize, int expireAfterWrite, TimeUnit timeUnit) {

        Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
        if (expireAfterWrite > 0) {
            cacheBuilder.expireAfterWrite(expireAfterWrite, timeUnit);
        }
        if (maxSize > 0) {
            cacheBuilder.maximumSize(maxSize);
        }

        cacheBuilder.ticker(ticker);
        return new CaffeineCache(cacheName, cacheBuilder.build());
    }

    @Bean
    public Ticker ticker() {
        return Ticker.systemTicker();
    }
}

This example was adapted from Define multiple caches configurations with Spring and Caffeine where Ben Manes points out there is an adapter called Coffee Boots which features the requested behaviour: https://github.com/stepio/coffee-boots

Betatron answered 27/4, 2023 at 9:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.