From 8c2ace33cbfe35f9f132c7ec8ca7d3c3beb59f7a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 24 Jan 2013 17:16:29 -0800 Subject: [PATCH] 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 --- .../AnnotatedClassCacheableService.java | 7 +- .../cache/config/CacheableService.java | 5 +- .../cache/config/DefaultCacheableService.java | 7 ++ .../cache/annotation/CachePut.java | 12 ++- .../cache/annotation/Cacheable.java | 12 ++- .../SpringCacheAnnotationParser.java | 5 +- .../cache/config/CacheAdviceParser.java | 42 +++++----- .../cache/interceptor/CacheAspectSupport.java | 81 +++++++++++++------ .../interceptor/CacheEvictOperation.java | 3 +- .../cache/interceptor/CacheOperation.java | 8 +- .../cache/interceptor/CachePutOperation.java | 22 ++++- .../cache/interceptor/CacheableOperation.java | 22 ++++- .../interceptor/ExpressionEvaluator.java | 79 +++++++++++++----- .../cache/config/spring-cache-3.2.xsd | 48 ++++++++--- .../cache/config/AbstractAnnotationTests.java | 26 +++++- .../AnnotatedClassCacheableService.java | 11 ++- .../cache/config/CacheableService.java | 7 +- .../cache/config/DefaultCacheableService.java | 11 ++- .../interceptor/ExpressionEvalutatorTest.java | 54 ++++++++++++- .../cache/config/cache-advice.xml | 14 ++-- src/dist/changelog.txt | 1 + src/reference/docbook/cache.xml | 14 ++++ 22 files changed, 385 insertions(+), 106 deletions(-) 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 +