diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index 5e64cb7fcc7..b8108051798 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -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. @@ -25,6 +25,7 @@ import org.springframework.cache.annotation.Caching; /** * @author Costin Leau + * @author Phillip Webb */ @Cacheable("default") public class AnnotatedClassCacheableService implements CacheableService { @@ -40,6 +41,10 @@ public class AnnotatedClassCacheableService implements CacheableService return null; } + public Object unless(int arg) { + return arg; + } + @CacheEvict("default") public void invalidate(Object arg1) { } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java index 09ad7df9986..98c1da1e031 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java @@ -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. @@ -20,6 +20,7 @@ package org.springframework.cache.config; * Basic service interface. * * @author Costin Leau + * @author Phillip Webb */ public interface CacheableService { @@ -39,6 +40,8 @@ public interface CacheableService { T conditional(int field); + T unless(int arg); + T key(Object arg1, Object arg2); T name(Object arg1); diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java index 6d83d91c02d..d29d43894e0 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -27,6 +27,7 @@ import org.springframework.cache.annotation.Caching; * Simple cacheable service * * @author Costin Leau + * @author Phillip Webb */ public class DefaultCacheableService implements CacheableService { @@ -78,6 +79,12 @@ public class DefaultCacheableService implements CacheableService { return counter.getAndIncrement(); } + @Override + @Cacheable(value = "default", unless = "#result > 10") + public Long unless(int arg) { + return (long) arg; + } + @Override @Cacheable(value = "default", key = "#p0") public Long key(Object arg1, Object arg2) { diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java index 0cc7f883f19..3ab2f52b0a0 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java @@ -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 { *

Default is "", meaning the method result is always cached. */ String condition() default ""; + + /** + * Spring Expression Language (SpEL) attribute used to veto the cache update. + *

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 ""; } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 716b840ad0e..1ab8df5762f 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -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 { *

Default is "", meaning the method is always cached. */ String condition() default ""; + + /** + * Spring Expression Language (SpEL) attribute used to veto method caching. + *

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 ""; } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java index 9114dfc6332..d8c7575ce7e 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -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; diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java index a1206c576f7..5092f474c73 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java @@ -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 } 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 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 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 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; + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 9e5162d6e3c..8e0b36889d1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -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 evictions) { - inspectCacheEvicts(evictions, true); + inspectCacheEvicts(evictions, true, ExpressionEvaluator.NO_RESULT); } - private void inspectAfterCacheEvicts(Collection evictions) { - inspectCacheEvicts(evictions, false); + private void inspectAfterCacheEvicts(Collection evictions, + Object result) { + inspectCacheEvicts(evictions, false, result); } - private void inspectCacheEvicts(Collection evictions, boolean beforeInvocation) { + private void inspectCacheEvicts(Collection 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 cacheables) { Map cUpdates = new LinkedHashMap(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 updates, Object retVal) { for (Map.Entry 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 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 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 getCaches() { return this.caches; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java index 62186d89661..f671c84f759 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvictOperation.java @@ -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; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index cd8a40ceb2c..46d84590e8e 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -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; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java index edb84ba156b..e6a61b0adfb 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java @@ -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; + } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java index 23c0b20e2c8..f9375a9a54e 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java @@ -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; + } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java index 9bcce589edc..ba109397a22 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java @@ -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; *

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 keyCache = new ConcurrentHashMap(64); + private final Map conditionCache = new ConcurrentHashMap(64); - private final Map keyCache = new ConcurrentHashMap(64); + private final Map unlessCache = new ConcurrentHashMap(64); private final Map targetMethodCache = new ConcurrentHashMap(64); - public EvaluationContext createEvaluationContext( - Collection 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 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 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 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) { diff --git a/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-3.2.xsd b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-3.2.xsd index 9bfdf140102..baf1f802c0a 100644 --- a/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-3.2.xsd +++ b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-3.2.xsd @@ -36,7 +36,7 @@ @@ -150,7 +150,7 @@ @@ -160,7 +160,7 @@ - + @@ -194,16 +194,42 @@ example, 'get*', 'handle*', '*Order', 'on*Event', etc.]]> - + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -217,16 +243,16 @@ - + - + diff --git a/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java b/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java index 4bc0c888b31..41d717d6927 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 the original author or authors. + * Copyright 2010-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. @@ -16,6 +16,8 @@ package org.springframework.cache.config; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.*; import java.util.Collection; @@ -33,6 +35,7 @@ import org.springframework.context.ApplicationContext; * * @author Costin Leau * @author Chris Beams + * @author Phillip Webb */ public abstract class AbstractAnnotationTests { @@ -187,6 +190,15 @@ public abstract class AbstractAnnotationTests { assertSame(r3, r4); } + public void testUnlessExpression(CacheableService service) throws Exception { + Cache cache = cm.getCache("default"); + cache.clear(); + service.unless(10); + service.unless(11); + assertThat(cache.get(10).get(), equalTo((Object) 10L)); + assertThat(cache.get(11), nullValue()); + } + public void testKeyExpression(CacheableService service) throws Exception { Object r1 = service.key(5, 1); Object r2 = service.key(5, 2); @@ -441,6 +453,16 @@ public abstract class AbstractAnnotationTests { testConditionalExpression(cs); } + @Test + public void testUnlessExpression() throws Exception { + testUnlessExpression(cs); + } + + @Test + public void testClassCacheUnlessExpression() throws Exception { + testUnlessExpression(cs); + } + @Test public void testKeyExpression() throws Exception { testKeyExpression(cs); @@ -618,4 +640,4 @@ public abstract class AbstractAnnotationTests { public void testClassMultiConditionalCacheAndEvict() { testMultiConditionalCacheAndEvict(ccs); } -} \ No newline at end of file +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index 4c72d9d5937..f168ab990ad 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -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. @@ -25,6 +25,7 @@ import org.springframework.cache.annotation.Caching; /** * @author Costin Leau + * @author Phillip Webb */ @Cacheable("default") public class AnnotatedClassCacheableService implements CacheableService { @@ -42,6 +43,12 @@ public class AnnotatedClassCacheableService implements CacheableService return null; } + @Override + @Cacheable(value = "default", unless = "#result > 10") + public Object unless(int arg) { + return arg; + } + @Override @CacheEvict("default") public void invalidate(Object arg1) { @@ -157,4 +164,4 @@ public class AnnotatedClassCacheableService implements CacheableService public Object multiUpdate(Object arg1) { return arg1; } -} \ No newline at end of file +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheableService.java b/spring-context/src/test/java/org/springframework/cache/config/CacheableService.java index 81dd758d19d..98c1da1e031 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/CacheableService.java +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheableService.java @@ -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. @@ -20,6 +20,7 @@ package org.springframework.cache.config; * Basic service interface. * * @author Costin Leau + * @author Phillip Webb */ public interface CacheableService { @@ -39,6 +40,8 @@ public interface CacheableService { T conditional(int field); + T unless(int arg); + T key(Object arg1, Object arg2); T name(Object arg1); @@ -67,4 +70,4 @@ public interface CacheableService { T multiConditionalCacheAndEvict(Object arg1); T multiUpdate(Object arg1); -} \ No newline at end of file +} diff --git a/spring-context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/spring-context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java index f9f19b47594..f987d7bbe61 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java +++ b/spring-context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -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. @@ -27,6 +27,7 @@ import org.springframework.cache.annotation.Caching; * Simple cacheable service * * @author Costin Leau + * @author Phillip Webb */ public class DefaultCacheableService implements CacheableService { @@ -78,6 +79,12 @@ public class DefaultCacheableService implements CacheableService { return counter.getAndIncrement(); } + @Override + @Cacheable(value = "default", unless = "#result > 10") + public Long unless(int arg) { + return (long) arg; + } + @Override @Cacheable(value = "default", key = "#p0") public Long key(Object arg1, Object arg2) { @@ -163,4 +170,4 @@ public class DefaultCacheableService implements CacheableService { public Long multiUpdate(Object arg1) { return Long.valueOf(arg1.toString()); } -} \ No newline at end of file +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvalutatorTest.java b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvalutatorTest.java index b52f364746a..0a664afac0e 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvalutatorTest.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/ExpressionEvalutatorTest.java @@ -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. @@ -16,24 +16,39 @@ package org.springframework.cache.interceptor; -import static org.junit.Assert.*; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import java.lang.reflect.Method; import java.util.Collection; import java.util.Iterator; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.cache.Cache; import org.springframework.cache.annotation.AnnotationCacheOperationSource; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.ReflectionUtils; import edu.emory.mathcs.backport.java.util.Collections; +/** + * @author Costin Leau + * @author Phillip Webb + */ public class ExpressionEvalutatorTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + private ExpressionEvaluator eval = new ExpressionEvaluator(); private AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); @@ -59,6 +74,7 @@ public class ExpressionEvalutatorTest { } @Test + @SuppressWarnings("unchecked") public void testMultipleCachingEval() throws Exception { AnnotatedClass target = new AnnotatedClass(); Method method = ReflectionUtils.findMethod(AnnotatedClass.class, "multipleCaching", Object.class, @@ -78,9 +94,41 @@ public class ExpressionEvalutatorTest { assertEquals(args[1], keyB); } + @Test + public void withReturnValue() throws Exception { + EvaluationContext context = createEvaluationContext("theResult"); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value, equalTo((Object) "theResult")); + } + + @Test + public void withNullReturn() throws Exception { + EvaluationContext context = createEvaluationContext(null); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value, nullValue()); + } + + @Test + public void withoutReturnValue() throws Exception { + EvaluationContext context = createEvaluationContext(ExpressionEvaluator.NO_RESULT); + Object value = new SpelExpressionParser().parseExpression("#result").getValue(context); + assertThat(value, nullValue()); + } + + private EvaluationContext createEvaluationContext(Object result) { + AnnotatedClass target = new AnnotatedClass(); + Method method = ReflectionUtils.findMethod(AnnotatedClass.class, "multipleCaching", Object.class, + Object.class); + Object[] args = new Object[] { new Object(), new Object() }; + @SuppressWarnings("unchecked") + Collection map = Collections.singleton(new ConcurrentMapCache("test")); + EvaluationContext context = eval.createEvaluationContext(map, method, args, target, target.getClass(), result); + return context; + } + private static class AnnotatedClass { @Caching(cacheable = { @Cacheable(value = "test", key = "#a"), @Cacheable(value = "test", key = "#b") }) public void multipleCaching(Object a, Object b) { } } -} \ No newline at end of file +} diff --git a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml index d9eef4816bb..5ebf54ddcb4 100644 --- a/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml +++ b/spring-context/src/test/resources/org/springframework/cache/config/cache-advice.xml @@ -3,7 +3,7 @@ xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:p="http://www.springframework.org/schema/p" - xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> @@ -11,6 +11,7 @@ + @@ -54,6 +55,7 @@ + @@ -80,9 +82,9 @@ - + - + @@ -98,12 +100,12 @@ - + - + - + diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 70defd90ddc..476a1c5bd5d 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -11,6 +11,7 @@ Changes in version 3.2.2 (2013-03-07) * DefaultMessageListenerContainer logs recovery failures at error level and exposes "isRecovering()" method (SPR-10230) * MediaType throws dedicated InvalidMediaTypeException instead of generic IllegalArgumentException (SPR-10226) * consistent use of LinkedHashMaps and independent getAttributeNames Enumeration in Servlet/Portlet mocks (SPR-10224) +* support 'unless' expression for cache veto (SPR-8871) Changes in version 3.2.1 (2013-01-24) diff --git a/src/reference/docbook/cache.xml b/src/reference/docbook/cache.xml index 08d2f34c0e1..2ed4ab2b3ff 100644 --- a/src/reference/docbook/cache.xml +++ b/src/reference/docbook/cache.xml @@ -152,6 +152,13 @@ public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + + In addition the conditional parameter, the unless parameter can be used to veto the adding of a value to the cache. Unlike + conditional, unless SpEL expressions are evalulated after the method has been called. Expanding + on the previous example - perhaps we only want to cache paperback books: + +
@@ -218,6 +225,13 @@ public Book findBook(String name)]]> stands for the argument index (starting from 0). iban or a0 (one can also use p0 or ]]> notation as an alias). + + result + evaluation context + The result of the method call (the value to be cached). Only available in 'unless' expressions and 'cache evict' + expression (when beforeInvocation is false). + #result +