How do I use the key, in a condition in Cacheable annotation

Nandita Rao picture Nandita Rao · Sep 8, 2015 · Viewed 15.8k times · Source

I'm caching the results of a function using the @cacheable annotation. I have 3 different caches and the key for each one is the user id of the currently logged in user concatenated with an argument in the method . On a certain event I want to evict all the cache entries which have the key that starts with that particular user id. For example :

@Cacheable(value = "testCache1", key = "'abcdef'")

I want the cache evict annotation to be something like :

@CacheEvict(value = "getSimilarVendors", condition = "key.startsWith('abc')")

But when I try to implement this it gives me an error :

Property or field 'key' cannot be found on object of type'org.springframework.cache.interceptor.CacheExpressionRootObject' - maybe not      public?

What is the correct way to implement this?

Answer

John Blum picture John Blum · Sep 9, 2015

All of the Spring Cache annotations (i.e. @Cacheable, @CacheEvict, etc) work on 1 cache entry per operation. @CacheEvict does support clearing the entire cache (with the allEntries attribute, however ignores the key in this case), but it is not selective (capable) in clearing a partial set of entries based on a key pattern in a single operation as you have described.

The main reason behind this is the Spring Cache interface abstraction itself, where the evict(key:Object) method takes a single key argument. But technically, it actually depends on the underlying Cache implementation (e.g. GemfireCache), which would need to support eviction on all entries who's keys match a particular pattern, which is typically, not the case for most caches (e.g. certainly not for GemFire, and not for Google Guava Cache either; see here and here.)

That is not to say you absolutely cannot achieve your goal. It's just not something supported out-of-the-box.

The interesting thing, minus some technical issues with your approach, is that your condition achieves sort of what you want... a cache eviction only occurs if the key satisfies the condition. However, you @CacheEvict annotated method is just missing the "key", hence the error. So, something like the following would satisfy the SpEL in your condition...

@CacheEvict(condition = "#key.startsWith('abc')")
public void someMethod(String key) {
  ...
}

However, you have to specify the key as an argument in this case. But, you don't want a specific key, you want a pattern matching several keys. So, forgo the condition and just use...

@CacheEvict
public void someMethod(String keyPattern) {
  ...
}

By way of example, using Guava as the caching provider, you would now need to provide a "custom" implementation extending GuavaCache.

public class CustomGuavaCache extends org.springframework.cache.guava.GuavaCache {

  protected boolean isMatch(String key, String pattern) {
    ...
  }

  protected boolean isPattern(String key) {
    ...
  }

  @Override
  public void evict(Object key) {
    if (key instanceof String && isPattern(key.toString()))) {
        Map<String, Object> entries = this.cache.asMap();
        Set<String> matchingKeys = new HashSet<>(entries.size());
        for (String actualKey : entries.keySet()) {
          if (isMatch(actualKey, key.toString()) {
            matchingKeys.add(actualKey);
          }
        }
        this.cache.invalidateAll(matchingKeys);
    }
    else {
      this.cache.invalidate(key);
    }
  }
}

Now just extend the GuavaCacheManager to plugin your "custom" GuavaCache (CustomGuavaCache)...

public class CustomGuavaCacheManager extends org.springframework.cache.guava.GuavaCacheManager {

  @Override
  protected Cache createGuavaCache(String name) {
    return new CustomGuavaCache(name, createNativeGuavaCache(name), isAllowNullValues());
  }
}

This approach takes advantage of Guava's Cache's invalidateAll(keys:Iterable) method. And, of course, you could use Java's Regex support to perform the "matching" on the desired keys to be evicted inside the isMatch(key, pattern) method.

So, I have not tested this, but this (or something similar) should achieve (almost) what you want (fingers crossed ;-)

Hope this helps!

Cheers, John