Support 'unless' expression for cache veto

Allow @Cachable, @CachePut and equivalent XML configuration to provide
a SpEL expression that can be used to veto putting an item into the
cache. Unlike 'condition' the 'unless' parameter is evaluated after
the method has been called and can therefore reference the #result.

For example:

    @Cacheable(value="book",
        condition="#name.length < 32",
        unless="#result.hardback")

This commit also allows #result to be referenced from @CacheEvict
expressions as long as 'beforeInvocation' is false.

Issue: SPR-8871
This commit is contained in:
Phillip Webb
2013-01-24 17:16:29 -08:00
parent 3252cb5a0f
commit 8c2ace33cb
22 changed files with 385 additions and 106 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -32,6 +32,7 @@ import org.springframework.cache.Cache;
* always causes the method to be invoked and its result to be placed into the cache.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@@ -58,4 +59,13 @@ public @interface CachePut {
* <p>Default is "", meaning the method result is always cached.
*/
String condition() default "";
/**
* Spring Expression Language (SpEL) attribute used to veto the cache update.
* <p>Unlike {@link #condition()}, this expression is evaluated after the method
* has been called and can therefore refer to the {@code result}. Default is "",
* meaning that caching is never vetoed.
* @since 3.2
*/
String unless() default "";
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -30,6 +30,7 @@ import java.lang.annotation.Target;
* returned instance is used as the cache value.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@@ -56,4 +57,13 @@ public @interface Cacheable {
* <p>Default is "", meaning the method is always cached.
*/
String condition() default "";
/**
* Spring Expression Language (SpEL) attribute used to veto method caching.
* <p>Unlike {@link #condition()}, this expression is evaluated after the method
* has been called and can therefore refer to the {@code result}. Default is "",
* meaning that caching is never vetoed.
* @since 3.2
*/
String unless() default "";
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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,7 @@ import org.springframework.util.ObjectUtils;
* @author Costin Leau
* @author Juergen Hoeller
* @author Chris Beams
* @author Phillip Webb
* @since 3.1
*/
@SuppressWarnings("serial")
@@ -82,6 +83,7 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
CacheableOperation cuo = new CacheableOperation();
cuo.setCacheNames(caching.value());
cuo.setCondition(caching.condition());
cuo.setUnless(caching.unless());
cuo.setKey(caching.key());
cuo.setName(ae.toString());
return cuo;
@@ -102,6 +104,7 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
CachePutOperation cuo = new CachePutOperation();
cuo.setCacheNames(caching.value());
cuo.setCondition(caching.condition());
cuo.setUnless(caching.unless());
cuo.setKey(caching.key());
cuo.setName(ae.toString());
return cuo;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -44,6 +44,7 @@ import org.w3c.dom.Element;
* BeanDefinitionParser} for the {@code <tx:advice/>} tag.
*
* @author Costin Leau
* @author Phillip Webb
*/
class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
@@ -54,7 +55,9 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
*/
private static class Props {
private String key, condition, method;
private String key;
private String condition;
private String method;
private String[] caches = null;
Props(Element root) {
@@ -70,13 +73,9 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
<T extends CacheOperation> T merge(Element element, ReaderContext readerCtx, T op) {
String cache = element.getAttribute("cache");
String k = element.getAttribute("key");
String c = element.getAttribute("condition");
String[] localCaches = caches;
String localKey = key, localCondition = condition;
// sanity check
String[] localCaches = caches;
if (StringUtils.hasText(cache)) {
localCaches = StringUtils.commaDelimitedListToStringArray(cache.trim());
} else {
@@ -84,17 +83,10 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
readerCtx.error("No cache specified specified for " + element.getNodeName(), element);
}
}
if (StringUtils.hasText(k)) {
localKey = k.trim();
}
if (StringUtils.hasText(c)) {
localCondition = c.trim();
}
op.setCacheNames(localCaches);
op.setKey(localKey);
op.setCondition(localCondition);
op.setKey(getAttributeValue(element, "key", this.key));
op.setCondition(getAttributeValue(element, "condition", this.condition));
return op;
}
@@ -165,7 +157,8 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
String name = prop.merge(opElement, parserContext.getReaderContext());
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation());
CacheableOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation());
op.setUnless(getAttributeValue(opElement, "unless", ""));
Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
if (col == null) {
@@ -207,7 +200,8 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
String name = prop.merge(opElement, parserContext.getReaderContext());
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CachePutOperation());
CachePutOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CachePutOperation());
op.setUnless(getAttributeValue(opElement, "unless", ""));
Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
if (col == null) {
@@ -222,4 +216,14 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpMap);
return attributeSourceDefinition;
}
private static String getAttributeValue(Element element, String attributeName, String defaultValue) {
String attribute = element.getAttribute(attributeName);
if(StringUtils.hasText(attribute)) {
return attribute.trim();
}
return defaultValue;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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.
@@ -56,6 +56,7 @@ import org.springframework.util.StringUtils;
* @author Costin Leau
* @author Juergen Hoeller
* @author Chris Beams
* @author Phillip Webb
* @since 3.1
*/
public abstract class CacheAspectSupport implements InitializingBean {
@@ -212,7 +213,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
retVal = invoker.invoke();
inspectAfterCacheEvicts(ops.get(EVICT));
inspectAfterCacheEvicts(ops.get(EVICT), retVal);
if (!updates.isEmpty()) {
update(updates, retVal);
@@ -225,14 +226,16 @@ public abstract class CacheAspectSupport implements InitializingBean {
}
private void inspectBeforeCacheEvicts(Collection<CacheOperationContext> evictions) {
inspectCacheEvicts(evictions, true);
inspectCacheEvicts(evictions, true, ExpressionEvaluator.NO_RESULT);
}
private void inspectAfterCacheEvicts(Collection<CacheOperationContext> evictions) {
inspectCacheEvicts(evictions, false);
private void inspectAfterCacheEvicts(Collection<CacheOperationContext> evictions,
Object result) {
inspectCacheEvicts(evictions, false, result);
}
private void inspectCacheEvicts(Collection<CacheOperationContext> evictions, boolean beforeInvocation) {
private void inspectCacheEvicts(Collection<CacheOperationContext> evictions,
boolean beforeInvocation, Object result) {
if (!evictions.isEmpty()) {
@@ -242,7 +245,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
CacheEvictOperation evictOp = (CacheEvictOperation) context.operation;
if (beforeInvocation == evictOp.isBeforeInvocation()) {
if (context.isConditionPassing()) {
if (context.isConditionPassing(result)) {
// for each cache
// lazy key initialization
Object key = null;
@@ -278,7 +281,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
private CacheStatus inspectCacheables(Collection<CacheOperationContext> cacheables) {
Map<CacheOperationContext, Object> cUpdates = new LinkedHashMap<CacheOperationContext, Object>(cacheables.size());
boolean updateRequire = false;
boolean updateRequired = false;
Object retVal = null;
if (!cacheables.isEmpty()) {
@@ -305,7 +308,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
boolean localCacheHit = false;
// check whether the cache needs to be inspected or not (the method will be invoked anyway)
if (!updateRequire) {
if (!updateRequired) {
for (Cache cache : context.getCaches()) {
Cache.ValueWrapper wrapper = cache.get(key);
if (wrapper != null) {
@@ -317,7 +320,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
}
if (!localCacheHit) {
updateRequire = true;
updateRequired = true;
}
}
else {
@@ -329,7 +332,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
// return a status only if at least on cacheable matched
if (atLeastOnePassed) {
return new CacheStatus(cUpdates, updateRequire, retVal);
return new CacheStatus(cUpdates, updateRequired, retVal);
}
}
@@ -386,8 +389,11 @@ public abstract class CacheAspectSupport implements InitializingBean {
private void update(Map<CacheOperationContext, Object> updates, Object retVal) {
for (Map.Entry<CacheOperationContext, Object> entry : updates.entrySet()) {
for (Cache cache : entry.getKey().getCaches()) {
cache.put(entry.getValue(), retVal);
CacheOperationContext operationContext = entry.getKey();
if(operationContext.canPutToCache(retVal)) {
for (Cache cache : operationContext.getCaches()) {
cache.put(entry.getValue(), retVal);
}
}
}
}
@@ -427,30 +433,49 @@ public abstract class CacheAspectSupport implements InitializingBean {
private final CacheOperation operation;
private final Collection<Cache> caches;
private final Object target;
private final Method method;
private final Object[] args;
// context passed around to avoid multiple creations
private final EvaluationContext evalContext;
private final Object target;
private final Class<?> targetClass;
private final Collection<Cache> caches;
public CacheOperationContext(CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {
this.operation = operation;
this.caches = CacheAspectSupport.this.getCaches(operation);
this.target = target;
this.method = method;
this.args = args;
this.evalContext = evaluator.createEvaluationContext(caches, method, args, target, targetClass);
this.target = target;
this.targetClass = targetClass;
this.caches = CacheAspectSupport.this.getCaches(operation);
}
protected boolean isConditionPassing() {
return isConditionPassing(ExpressionEvaluator.NO_RESULT);
}
protected boolean isConditionPassing(Object result) {
if (StringUtils.hasText(this.operation.getCondition())) {
return evaluator.condition(this.operation.getCondition(), this.method, this.evalContext);
EvaluationContext evaluationContext = createEvaluationContext(result);
return evaluator.condition(this.operation.getCondition(), this.method,
evaluationContext);
}
return true;
}
protected boolean canPutToCache(Object value) {
String unless = "";
if (this.operation instanceof CacheableOperation) {
unless = ((CacheableOperation) this.operation).getUnless();
}
else if (this.operation instanceof CachePutOperation) {
unless = ((CachePutOperation) this.operation).getUnless();
}
if(StringUtils.hasText(unless)) {
EvaluationContext evaluationContext = createEvaluationContext(value);
return !evaluator.unless(unless, this.method, evaluationContext);
}
return true;
}
@@ -461,11 +486,17 @@ public abstract class CacheAspectSupport implements InitializingBean {
*/
protected Object generateKey() {
if (StringUtils.hasText(this.operation.getKey())) {
return evaluator.key(this.operation.getKey(), this.method, this.evalContext);
EvaluationContext evaluationContext = createEvaluationContext(ExpressionEvaluator.NO_RESULT);
return evaluator.key(this.operation.getKey(), this.method, evaluationContext);
}
return keyGenerator.generate(this.target, this.method, this.args);
}
private EvaluationContext createEvaluationContext(Object result) {
return evaluator.createEvaluationContext(this.caches, this.method, this.args,
this.target, this.targetClass, result);
}
protected Collection<Cache> getCaches() {
return this.caches;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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 @@ public class CacheEvictOperation extends CacheOperation {
private boolean cacheWide = false;
private boolean beforeInvocation = false;
public void setCacheWide(boolean cacheWide) {
this.cacheWide = cacheWide;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -119,10 +119,10 @@ public abstract class CacheOperation {
result.append(this.name);
result.append("] caches=");
result.append(this.cacheNames);
result.append(" | condition='");
result.append(this.condition);
result.append("' | key='");
result.append(" | key='");
result.append(this.key);
result.append("' | condition='");
result.append(this.condition);
result.append("'");
return result;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -20,8 +20,28 @@ package org.springframework.cache.interceptor;
* Class describing a cache 'put' operation.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
public class CachePutOperation extends CacheOperation {
private String unless;
public String getUnless() {
return unless;
}
public void setUnless(String unless) {
this.unless = unless;
}
@Override
protected StringBuilder getOperationDescription() {
StringBuilder sb = super.getOperationDescription();
sb.append(" | unless='");
sb.append(this.unless);
sb.append("'");
return sb;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 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.
@@ -20,8 +20,28 @@ package org.springframework.cache.interceptor;
* Class describing a cache 'cacheable' operation.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
public class CacheableOperation extends CacheOperation {
private String unless;
public String getUnless() {
return unless;
}
public void setUnless(String unless) {
this.unless = unless;
}
@Override
protected StringBuilder getOperationDescription() {
StringBuilder sb = super.getOperationDescription();
sb.append(" | unless='");
sb.append(this.unless);
sb.append("'");
return sb;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 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,49 +35,84 @@ import org.springframework.expression.spel.standard.SpelExpressionParser;
* <p>Performs internal caching for performance reasons.
*
* @author Costin Leau
* @author Phillip Webb
* @since 3.1
*/
class ExpressionEvaluator {
public static final Object NO_RESULT = new Object();
private final SpelExpressionParser parser = new SpelExpressionParser();
// shared param discoverer since it caches data internally
private final ParameterNameDiscoverer paramNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private final Map<String, Expression> keyCache = new ConcurrentHashMap<String, Expression>(64);
private final Map<String, Expression> conditionCache = new ConcurrentHashMap<String, Expression>(64);
private final Map<String, Expression> keyCache = new ConcurrentHashMap<String, Expression>(64);
private final Map<String, Expression> unlessCache = new ConcurrentHashMap<String, Expression>(64);
private final Map<String, Method> targetMethodCache = new ConcurrentHashMap<String, Method>(64);
public EvaluationContext createEvaluationContext(
Collection<Cache> caches, Method method, Object[] args, Object target, Class<?> targetClass) {
CacheExpressionRootObject rootObject =
new CacheExpressionRootObject(caches, method, args, target, targetClass);
return new LazyParamAwareEvaluationContext(rootObject,
this.paramNameDiscoverer, method, args, targetClass, this.targetMethodCache);
/**
* Create an {@link EvaluationContext} without a return value.
* @see #createEvaluationContext(Collection, Method, Object[], Object, Class, Object)
*/
public EvaluationContext createEvaluationContext(Collection<Cache> caches,
Method method, Object[] args, Object target, Class<?> targetClass) {
return createEvaluationContext(caches, method, args, target, targetClass,
NO_RESULT);
}
public boolean condition(String conditionExpression, Method method, EvaluationContext evalContext) {
String key = toString(method, conditionExpression);
Expression condExp = this.conditionCache.get(key);
if (condExp == null) {
condExp = this.parser.parseExpression(conditionExpression);
this.conditionCache.put(key, condExp);
/**
* Create an {@link EvaluationContext}.
*
* @param caches the current caches
* @param method the method
* @param args the method arguments
* @param target the target object
* @param targetClass the target class
* @param result the return value (can be {@code null}) or
* {@link #NO_RESULT} if there is no return at this time
* @return the evalulation context
*/
public EvaluationContext createEvaluationContext(Collection<Cache> caches,
Method method, Object[] args, Object target, Class<?> targetClass,
final Object result) {
CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches,
method, args, target, targetClass);
LazyParamAwareEvaluationContext evaluationContext = new LazyParamAwareEvaluationContext(rootObject,
this.paramNameDiscoverer, method, args, targetClass, this.targetMethodCache);
if(result != NO_RESULT) {
evaluationContext.setVariable("result", result);
}
return condExp.getValue(evalContext, boolean.class);
return evaluationContext;
}
public Object key(String keyExpression, Method method, EvaluationContext evalContext) {
String key = toString(method, keyExpression);
Expression keyExp = this.keyCache.get(key);
if (keyExp == null) {
keyExp = this.parser.parseExpression(keyExpression);
this.keyCache.put(key, keyExp);
return getExpression(this.keyCache, keyExpression, method).getValue(evalContext);
}
public boolean condition(String conditionExpression, Method method, EvaluationContext evalContext) {
return getExpression(this.conditionCache, conditionExpression, method).getValue(
evalContext, boolean.class);
}
public boolean unless(String unlessExpression, Method method, EvaluationContext evalContext) {
return getExpression(this.unlessCache, unlessExpression, method).getValue(
evalContext, boolean.class);
}
private Expression getExpression(Map<String, Expression> cache, String expression, Method method) {
String key = toString(method, expression);
Expression rtn = cache.get(key);
if (rtn == null) {
rtn = this.parser.parseExpression(expression);
cache.put(key, rtn);
}
return keyExp.getValue(evalContext);
return rtn;
}
private String toString(Method method, String expression) {