I found two native workarround methodologies to use a complex collection values as a cache key.
The first method is using a computed string as the cache key:
@Cacheable(value = "Words", key = "{#root.methodName, #a1}", unless = "#result == null")
//or
@Cacheable(value = "Words", key = "{#root.methodName, #p1}", unless = "#result == null")
//or
@Cacheable(value = "Words", key = "{#root.methodName, #precomputedString}", unless = "#result == null")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
In order to call to this method service as follows:
//use your own complex object collection to string mapping as a second parammeter
service.findWords(request.getParameterMap().values(),request.getParameterMap()
.values()
.stream()
.map(strings -> Arrays.stream(strings)
.collect(Collectors.joining(",")))
.collect(Collectors.joining(","));)
And the second methodology (my prefered form):
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}", unless = "#result == null")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
Where package.relationalDatabase.utils.Functions getSpringCacheKey is a own created function as follows:
public static String getSpringCacheKey(Object o) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
boolean isSpringEntity = o.getClass().getAnnotation(javax.persistence.Entity.class) != null;
if (isSpringEntity) {
return objectMapper.writerWithView(JSONViews.Simple.class).writeValueAsString(o);
} else {
return objectMapper.writeValueAsString(o);
}
}
Note I: this methodology allow the combination of the native key cache notation with a custom wrapper. Unlike Spring cache's keyGenerator property which does not allow the key annotation (they are mutually exclusive) and wich require the creatation of a CustomKeyGenerator
@Cacheable(value = "Edges", unless = "#result == null", keyGenerator = "CustomKeyGenerator")
public List<Edge> findWords(HttpServletRequest request, String precomputedStringKey) {
}
////////
public class CustomKeyGenerator implements KeyGenerator {
Object generate(Object target, Method method, Object... params)
}
And the creation of a return wrapper for each complex collection key. For example:
@Override
public Object generate(Object target, Method method, Object... params) {
if(params[0] instanceof Collection)
//do something
if(params[0] instanceof Map)
//do something
if(params[0] instanceof HttpServletRequest)
//do something
}
Therefore, the proposed methodology allows:
//note #request.getParameterMap().values()
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}"
//note #request.getParameterMap().keySet()
@Cacheable(value = "Edges", key = "{#root.methodName, T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().keySet())}"
without need to update the method for each collection.
Note II: This methodology alow the usage of jackson views for spring entities but in some cases there is needed the @JsonIgnoreProperties({"hibernateLazyInitializer"}) annotation.
Finally the trace result of spring cache for this methodology is the following:
Computed cache key '[findWords,
[[""],["0"],[""],[""],[""],[""],["brazil"],["on"],["false"]]]' for
operation Builder[public java.util.List
package.relationalDatabase.services.myClass.find(javax.servlet.http.HttpServletRequest)]
caches=[myClass] | key='{#root.methodName,
T(package.relationalDatabase.utils.Functions).getSpringCacheKey(#request.getParameterMap().values())}'
| keyGenerator='' | cacheManager='' | cacheResolver='' | condition=''
| unless='#result == null' | sync='false'
On the other hand, it is recommended to use string hash functions to compress the resulting key value.