Search code examples
javaspringspring-bootspring-cache

Spring idiomatic way to register CacheResolver in @Configuration class


I'm working with a Spring Boot application that needs to use a cache and using Spring caching framework, and I'm importing a library (that I have little control over) that also has a cache. I'm getting errors that there are multiple CacheManager implementations so I need to use a CacheResolver. So far makes perfect sense. Looked at CacheResolver interface, looks reasonable.

So I have created my own simple CacheResolver and created it as a bean, but it isn't being picked up automatically - ok, I'm used to spring magic but never mind, so how do I register it?

The only way I have managed to get a custom CacheResolver recognised by the framework so far is by making my @Configuration class implement CachingConfigurator interface and overriding the no method argument cacheResolver() - however that's frustratingly useless as I need to pass that method my 2x CacheManager implementations to add it to constructor of my custom CacheResolver - as soon as I add those beans as method arguments it stops working because, whilst the bean is registered normally, that particular interface method is no longer an override and so it doesn't get called as part of the CachingConfigurator lifecycle. It just doesn't feel like the intended/idiomatic way of doing things either but I've found no better documentation.

Importantly, I don't want to be putting cacheManager = xyz or cacheResolver = zyx in all my Cacheable annotations because, as I say, some of those annotations are in a library I don't own.

Anyone had to handle this situation? Feels like I'm missing something obvious. Thanks!


Solution

  • I would suggest using the Spring Framework ObjectProvider in your custom CacheResolver instead of specific or multiple CacheManager implementations. This way you are flexible and it really doesn't matter how many CacheManager instances there are.

    public class CustomCacheResolver implements CacheResolver {
     
      private final ObjectProvider<CacheManager> managers;
    
      public CustomCacheResolver(ObjectProvider<CacheManager> managers) {
        this.managers = managers;
      }
    
      public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
        var caches = new ArrayList();
        for (var cacheName : context.getOperation().getCacheNames()) {
          var cache = resolveCache(cacheName);
          if (cache == null) {
            throw new IllegalArgumentException("Cannot find cache named '" +
                            cacheName + "' for " + context.getOperation());
          }
          caches.add(cache);
    
        }    
        return caches;
      }
    
      private Cache resolveCache(String cacheName) {
        managers.stream().map((cm) -> cm.getCache(cacheName)).filter(Object::nonNull).findFirst().orElse(null);
    
      }
    
    }
    

    Something along these lines. This way it doesn't matter if there is 1 or 100 cachemanagers. They will be lazily resolved when the resolveCaches method is being called.

    The configuration can be adapted to accommodate this.

    @Configuration
    public MyCacheConfiguration implements CachingConfigurer {
    
      private final ObjectProvider<CacheManager> managers;
    
      public MyCacheConfiguration(ObjectProvider<CacheManager> managers) {
        this.managers = managers;
      }
    
      @Override
      public  CacheResolver cacheResolver() {
        return new CustomCacheResolver(managers);
      }
    }
    

    The benefit of using an ObjectProvider over a List is that with the List the CacheManager instances will be eagerly resolved for injection. This might then suddenly be earlier then it otherwise would be. With the ObjectProvider this is delayed until the moment they are actually needed.