'Spring Cache Caffeine bulk retrieval

Is it possible to use Caffeine's CacheLoader::loadAll with @Cacheable annotated method with collection parameter, like

@Cacheable(cacheNames = "exampleCache", cacheManager="exampleCacheManager", keyGenerator = "complexKeyGenerator")
   List<String> getItems(List<String> keys, String commonForEveryKey) {
      return ...
}

@Component
class ComplexKeyGenerator implements KeyGenerator {

      @Override
      public Object generate(Object target, Method method, Object... params) {
         return ((List<String>)params[0]).stream()
                     .map(item -> new ComplexKey(item, (String) params[1]))
                     .collect(Collectors.toList());
      }
}

@Data
   @AllArgsConstructor
   class ComplexKey {
      String key;
      String commonGuy;
}

class CustomCacheLoader implements CacheLoader<ComplexKey, String> {

      @Override
      public @Nullable String load(@NonNull ComplexKey key) throws Exception {
         return loadAll(List.of(key)).get(key);
      }

      @Override
      public @NonNull Map<@NonNull ComplexKey, @NonNull String> loadAll(@NonNull Iterable<? extends @NonNull ComplexKey> keys)
         throws Exception {
         return ...
      }
}

@Bean
   CacheManager exampleCacheManager(LoadingCache exampleCache) {
      CaffeineCacheManager cacheManager = new CaffeineCacheManager();
      cacheManager.registerCustomCache("exampleCache", exampleCache());
      return cacheManager;
}

   @Bean
   Cache<Object, Object> exampleCache() {
      return Caffeine.newBuilder()
                     .maximumSize(1000)
                     .expireAfterWrite(1, TimeUnit.HOURS)
                     .recordStats()
                     .build(new CustomCacheLoader());
}

Looks like Spring Cache invokes CustomCacheLoader::load instead of CustomCacheLoader::loadAll and fails on ClassCastException since it cannot cast collection of keys into single key.

What else should I configure to make it work?



Solution 1:[1]

Unfortunately Spring doesn't support retrieving of a collection of cached items by a collection of keys via @Cacheable mechanism.

Here's an issue on that: https://github.com/spring-projects/spring-framework/issues/23221

One option to achieve this is to use a custom library ( https://github.com/qaware/collection-cacheable-for-spring) that provides a @CollectionCacheable annotation:

@CollectionCacheable(cacheNames = "myCache")
Map<Long, MyEntity> findByIds(Collection<Long> ids) {
    // do efficient batch retrieve of many MyEntity's and build result map
}

If you'd really like to stick with the code you have you could generalize it to something similar:

@Component
class ComplexKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        if (params.length < 2 || !(params[0] instanceof Collection && params[1] instanceof String)) {
            return SimpleKeyGenerator.generateKey(params);
        }

        return ((Collection<String>) params[0]).stream()
                .map(item -> new ComplexKey(item, (String) params[1]))
                .collect(Collectors.toList());
    }
}

class CustomCacheLoader implements CacheLoader<Object, Object> {
    @Override
    public Object load(Object key) throws Exception {
        final Collection<Object> keys = (key instanceof Collection) ?
                ((Collection<Object>) key) : Collections.singletonList(key);
        final Collection<Object> values = new ArrayList<>(loadAll(keys).values());
        return values;
    }

    @Override
    public Map<Object, Object> loadAll(Iterable<?> keys) throws Exception {
        ...
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dekkard