Custom KeyGenerator

This commit adds an extra parameter to the base @Cache method
annotations: keyGenerator. This parameter holds the name of the
KeyGenerator bean to use to compute the key for that specific
caching endpoint.

This gives therefore a third way to customize the key. These are:
1. Default KeyGenerator (global for all endpoints)
2. The 'key' attribute of the annotation, giving the SpEL expression to use
3. The 'keyGenerator' attribute of the annotation

The annotation attributes are therefore exclusive. Trying to specify
them both will result in an IllegalStateException.

The KeyGenerator to use for a given operation is cached on startup
so that multiple calls to it does not resolve the instance to use over and
over again.

Issue: SPR-10629
This commit is contained in:
Stephane Nicoll
2014-01-10 16:24:51 +01:00
parent 906321dcdd
commit 81c208098f
25 changed files with 407 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,10 +45,17 @@ public @interface CacheEvict {
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
* <p>Default is "", meaning all method parameters are considered as a key.
* <p>Default is "", meaning all method parameters are considered as a key, unless
* a custom {@link #keyGenerator()} has been set.
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
* <p>Mutually exclusive with the {@link #key()} attribute.
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) attribute used for conditioning the method caching.
* <p>Default is "", meaning the method is always cached.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,10 +50,17 @@ public @interface CachePut {
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
* <p>Default is "", meaning all method parameters are considered as a key.
* <p>Default is "", meaning all method parameters are considered as a key, unless
* a custom {@link #keyGenerator()} has been set.
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
* <p>Mutually exclusive with the {@link #key()} attribute.
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) attribute used for conditioning the cache update.
* <p>Default is "", meaning the method result is always cached.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,10 +48,17 @@ public @interface Cacheable {
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.
* <p>Default is "", meaning all method parameters are considered as a key.
* <p>Default is "", meaning all method parameters are considered as a key, unless
* a custom {@link #keyGenerator()} has been set.
*/
String key() default "";
/**
* The bean name of the custom {@link org.springframework.cache.interceptor.KeyGenerator} to use.
* <p>Mutually exclusive with the {@link #key()} attribute.
*/
String keyGenerator() default "";
/**
* Spring Expression Language (SpEL) attribute used for conditioning the method caching.
* <p>Default is "", meaning the method is always cached.

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CachePutOperation;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Strategy implementation for parsing Spring's {@link Caching}, {@link Cacheable},
@@ -86,7 +87,10 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
cuo.setCondition(caching.condition());
cuo.setUnless(caching.unless());
cuo.setKey(caching.key());
cuo.setKeyGenerator(caching.keyGenerator());
cuo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
return cuo;
}
@@ -95,9 +99,12 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
ceo.setCacheNames(caching.value());
ceo.setCondition(caching.condition());
ceo.setKey(caching.key());
ceo.setKeyGenerator(caching.keyGenerator());
ceo.setCacheWide(caching.allEntries());
ceo.setBeforeInvocation(caching.beforeInvocation());
ceo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
return ceo;
}
@@ -107,7 +114,10 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
cuo.setCondition(caching.condition());
cuo.setUnless(caching.unless());
cuo.setKey(caching.key());
cuo.setKeyGenerator(caching.keyGenerator());
cuo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
return cuo;
}
@@ -159,6 +169,15 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return (anns.isEmpty() ? null : anns);
}
private void checkKeySourceConsistency(AnnotatedElement ae, String key, String keyGenerator) {
if (StringUtils.hasText(key) && StringUtils.hasText(keyGenerator)) {
throw new IllegalStateException("Invalid cache annotation configuration on '"
+ ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
}
@Override
public boolean equals(Object other) {
return (this == other || other instanceof SpringCacheAnnotationParser);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -168,7 +168,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
private static String getAttributeValue(Element element, String attributeName, String defaultValue) {
String attribute = element.getAttribute(attributeName);
if(StringUtils.hasText(attribute)) {
if (StringUtils.hasText(attribute)) {
return attribute.trim();
}
return defaultValue;
@@ -184,6 +184,8 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
private String key;
private String keyGenerator;
private String condition;
private String method;
@@ -194,6 +196,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
Props(Element root) {
String defaultCache = root.getAttribute("cache");
key = root.getAttribute("key");
keyGenerator = root.getAttribute("key-generator");
condition = root.getAttribute("condition");
method = root.getAttribute(METHOD_ATTRIBUTE);
@@ -218,8 +221,16 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
op.setCacheNames(localCaches);
op.setKey(getAttributeValue(element, "key", this.key));
op.setKeyGenerator(getAttributeValue(element, "key-generator", this.keyGenerator));
op.setCondition(getAttributeValue(element, "condition", this.condition));
if (StringUtils.hasText(op.getKey()) && StringUtils.hasText(op.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache advice configuration on '"
+ element.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
return op;
}

View File

@@ -29,9 +29,12 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -67,7 +70,7 @@ import org.springframework.util.StringUtils;
* @author Stephane Nicoll
* @since 3.1
*/
public abstract class CacheAspectSupport implements InitializingBean {
public abstract class CacheAspectSupport implements InitializingBean, ApplicationContextAware {
protected final Log logger = LogFactory.getLog(getClass());
@@ -79,6 +82,8 @@ public abstract class CacheAspectSupport implements InitializingBean {
private KeyGenerator keyGenerator = new SimpleKeyGenerator();
private ApplicationContext applicationContext;
private boolean initialized = false;
@@ -116,24 +121,31 @@ public abstract class CacheAspectSupport implements InitializingBean {
}
/**
* Set the KeyGenerator for this cache aspect.
* The default is a {@link SimpleKeyGenerator}.
* Set the default {@link KeyGenerator} that this cache aspect should delegate to
* if no specific key generator has been set for the operation.
* <p>The default is a {@link SimpleKeyGenerator}
*/
public void setKeyGenerator(KeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
}
/**
* Return the KeyGenerator for this cache aspect,
* Return the default {@link KeyGenerator} that this cache aspect delegates to.
*/
public KeyGenerator getKeyGenerator() {
return this.keyGenerator;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void afterPropertiesSet() {
Assert.state(this.cacheManager != null, "Property 'cacheManager' is required");
Assert.state(this.cacheOperationSource != null, "Property 'cacheOperationSources' is required: " +
"If there are no cacheable methods, then don't use a cache aspect.");
Assert.state(this.applicationContext != null, "The application context was not injected as it should.");
this.initialized = true;
}
@@ -372,14 +384,22 @@ public abstract class CacheAspectSupport implements InitializingBean {
private final Collection<? extends Cache> caches;
private final KeyGenerator operationKeyGenerator;
public CacheOperationContext(CacheOperation operation, Method method,
Object[] args, Object target, Class<?> targetClass) {
Object[] args, Object target, Class<?> targetClass) {
this.operation = operation;
this.method = method;
this.args = extractArgs(method, args);
this.target = target;
this.targetClass = targetClass;
this.caches = CacheAspectSupport.this.getCaches(operation);
if (StringUtils.hasText(operation.getKeyGenerator())) { // TODO: exception mgt?
this.operationKeyGenerator = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
applicationContext, KeyGenerator.class, operation.getKeyGenerator());
} else {
this.operationKeyGenerator = keyGenerator;
}
}
private Object[] extractArgs(Method method, Object[] args) {
@@ -405,8 +425,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
String unless = "";
if (this.operation instanceof CacheableOperation) {
unless = ((CacheableOperation) this.operation).getUnless();
}
else if (this.operation instanceof CachePutOperation) {
} else if (this.operation instanceof CachePutOperation) {
unless = ((CachePutOperation) this.operation).getUnless();
}
if (StringUtils.hasText(unless)) {
@@ -425,7 +444,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.key(this.operation.getKey(), this.method, evaluationContext);
}
return keyGenerator.generate(this.target, this.method, this.args);
return operationKeyGenerator.generate(this.target, this.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,8 @@ public abstract class CacheOperation {
private String key = "";
private String keyGenerator = "";
private String name = "";
@@ -50,6 +52,10 @@ public abstract class CacheOperation {
return key;
}
public String getKeyGenerator() {
return keyGenerator;
}
public String getName() {
return name;
}
@@ -77,6 +83,11 @@ public abstract class CacheOperation {
this.key = key;
}
public void setKeyGenerator(String keyGenerator) {
Assert.notNull(keyGenerator);
this.keyGenerator = keyGenerator;
}
public void setName(String name) {
Assert.hasText(name);
this.name = name;
@@ -124,6 +135,8 @@ public abstract class CacheOperation {
result.append(this.cacheNames);
result.append(" | key='");
result.append(this.key);
result.append("' | keyGenerator='");
result.append(this.keyGenerator);
result.append("' | condition='");
result.append(this.condition);
result.append("'");