Steve
Steve

Reputation: 55555

Multiple Caffeine LoadingCaches added to Spring CaffeineCacheManager

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?

Upvotes: 16

Views: 14834

Answers (4)

Georg S.
Georg S.

Reputation: 23

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

Upvotes: 1

Ali Katkar
Ali Katkar

Reputation: 557

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
    }

Upvotes: 1

gabriel119435
gabriel119435

Reputation: 6792

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();
    }
}

Upvotes: 3

Stephane Nicoll
Stephane Nicoll

Reputation: 33091

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.

Upvotes: 32

Related Questions