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?
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