From eddb0ac3bed5f1398bc5b44d35e1bcbe37566359 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Wed, 9 Nov 2011 10:00:44 +0000 Subject: [PATCH] + introduced @CacheUpdate annotation + introduced @CacheDefinition annotation + introduced meta-annotation to allow multiple @Cache annotations SPR-7833 SPR-8082 --- .../AnnotationCacheOperationSource.java | 30 +- .../annotation/CacheAnnotationParser.java | 6 +- .../cache/annotation/CacheDefinition.java | 43 +++ .../cache/annotation/CacheUpdate.java | 60 ++++ .../cache/annotation/Cacheable.java | 1 - .../SpringCacheAnnotationParser.java | 78 ++++- .../cache/annotation/package-info.java | 3 +- .../cache/config/CacheAdviceParser.java | 4 +- .../AbstractFallbackCacheOperationSource.java | 37 +-- .../cache/interceptor/CacheAspectSupport.java | 274 +++++++++++++----- .../interceptor/CacheOperationEditor.java | 2 +- .../interceptor/CacheOperationSource.java | 5 +- .../CacheOperationSourcePointcut.java | 3 +- .../interceptor/CacheUpdateOperation.java | 2 +- .../cache/interceptor/CacheableOperation.java | 27 ++ .../CompositeCacheOperationSource.java | 20 +- .../NameMatchCacheOperationSource.java | 10 +- .../cache/config/AbstractAnnotationTests.java | 45 +++ .../AnnotatedClassCacheableService.java | 11 + .../cache/config/CacheableService.java | 4 + .../cache/config/DefaultCacheableService.java | 11 + 21 files changed, 540 insertions(+), 136 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheDefinition.java create mode 100644 org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheUpdate.java create mode 100644 org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index 05f9d8a3620..0851d702656 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -19,6 +19,8 @@ package org.springframework.cache.annotation; import java.io.Serializable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; @@ -32,7 +34,7 @@ import org.springframework.util.Assert; * Implementation of the {@link org.springframework.cache.interceptor.CacheOperationSource} * interface for working with caching metadata in JDK 1.5+ annotation format. * - *

This class reads Spring's JDK 1.5+ {@link Cacheable} and {@link CacheEvict} + *

This class reads Spring's JDK 1.5+ {@link Cacheable}, {@link CacheUpdate} and {@link CacheEvict} * annotations and exposes corresponding caching operation definition to Spring's cache infrastructure. * This class may also serve as base class for a custom CacheOperationSource. * @@ -83,13 +85,13 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati @Override - protected CacheOperation findCacheOperation(Class clazz) { - return determineCacheOperation(clazz); + protected Collection findCacheOperations(Class clazz) { + return determineCacheOperations(clazz); } @Override - protected CacheOperation findCacheOperation(Method method) { - return determineCacheOperation(method); + protected Collection findCacheOperations(Method method) { + return determineCacheOperations(method); } /** @@ -103,14 +105,19 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati * @return CacheOperation the configured caching operation, * or null if none was found */ - protected CacheOperation determineCacheOperation(AnnotatedElement ae) { + protected Collection determineCacheOperations(AnnotatedElement ae) { + Collection ops = null; + for (CacheAnnotationParser annotationParser : this.annotationParsers) { - CacheOperation attr = annotationParser.parseCacheAnnotation(ae); - if (attr != null) { - return attr; + Collection annOps = annotationParser.parseCacheAnnotations(ae); + if (annOps != null) { + if (ops == null) { + ops = new ArrayList(); + } + ops.addAll(annOps); } } - return null; + return ops; } /** @@ -120,5 +127,4 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati protected boolean allowPublicMethodsOnly() { return this.publicMethodsOnly; } - -} +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java index b016f3009cd..44117112c71 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java @@ -17,6 +17,7 @@ package org.springframework.cache.annotation; import java.lang.reflect.AnnotatedElement; +import java.util.Collection; import org.springframework.cache.interceptor.CacheOperation; @@ -25,7 +26,7 @@ import org.springframework.cache.interceptor.CacheOperation; * Strategy interface for parsing known caching annotation types. * {@link AnnotationCacheDefinitionSource} delegates to such * parsers for supporting specific annotation types such as Spring's own - * {@link Cacheable} or {@link CacheEvict}. + * {@link Cacheable}, {@link CacheUpdate} or {@link CacheEvict}. * * @author Costin Leau * @since 3.1 @@ -43,6 +44,5 @@ public interface CacheAnnotationParser { * or null if none was found * @see AnnotationCacheOperationSource#determineCacheOperation */ - CacheOperation parseCacheAnnotation(AnnotatedElement ae); - + Collection parseCacheAnnotations(AnnotatedElement ae); } diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheDefinition.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheDefinition.java new file mode 100644 index 00000000000..7151a3470d1 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheDefinition.java @@ -0,0 +1,43 @@ +/* + * Copyright 2011 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Group annotation for multiple cacheable annotations (of different or the same type). + * + * @author Costin Leau + * @since 3.1 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface CacheDefinition { + + Cacheable[] cacheables(); + + CacheUpdate[] updates(); + + CacheEvict[] evicts(); +} diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheUpdate.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheUpdate.java new file mode 100644 index 00000000000..fee6fb19c7f --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/CacheUpdate.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * + * Annotation indicating that a method (or all methods on a class) trigger(s) + * a cache update operation. As opposed to {@link Cacheable} annotation, this annotation + * does not cause the target method to be skipped in case of a cache hit - rather it + * always causes the method to be invoked and its result to be placed into the cache. + * + * @author Costin Leau + * @since 3.1 + */ +@Target({ ElementType.METHOD, ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface CacheUpdate { + + /** + * Name of the caches in which the update takes place. + *

May be used to determine the target cache (or caches), matching the + * qualifier value (or the bean name(s)) of (a) specific bean definition. + */ + String[] value(); + + /** + * Spring Expression Language (SpEL) attribute for computing the key dynamically. + *

Default is "", meaning all method parameters are considered as a key. + */ + String key() default ""; + + /** + * Spring Expression Language (SpEL) attribute used for conditioning the cache update. + *

Default is "", meaning the method result is always cached. + */ + String condition() default ""; +} diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 132352dbaee..c6a33e9a5d7 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -56,5 +56,4 @@ public @interface Cacheable { *

Default is "", meaning the method is always cached. */ String condition() default ""; - } diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java index df99b7f73ec..0ec6c4b5c49 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -18,14 +18,18 @@ package org.springframework.cache.annotation; import java.io.Serializable; import java.lang.reflect.AnnotatedElement; +import java.util.ArrayList; +import java.util.Collection; import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CacheUpdateOperation; +import org.springframework.cache.interceptor.CacheableOperation; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; /** - * Strategy implementation for parsing Spring's {@link Cacheable} and {@link CacheEvict} annotations. + * Strategy implementation for parsing Spring's {@link Cacheable}, {@link CacheEvict} and {@link CacheUpdate} annotations. * * @author Costin Leau * @author Juergen Hoeller @@ -34,20 +38,38 @@ import org.springframework.core.annotation.AnnotationUtils; @SuppressWarnings("serial") public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable { - public CacheOperation parseCacheAnnotation(AnnotatedElement ae) { - Cacheable update = AnnotationUtils.getAnnotation(ae, Cacheable.class); - if (update != null) { - return parseCacheableAnnotation(ae, update); + public Collection parseCacheAnnotations(AnnotatedElement ae) { + Collection ops = null; + + Cacheable cache = AnnotationUtils.getAnnotation(ae, Cacheable.class); + if (cache != null) { + ops = lazyInit(ops); + ops.add(parseCacheableAnnotation(ae, cache)); } CacheEvict evict = AnnotationUtils.getAnnotation(ae, CacheEvict.class); if (evict != null) { - return parseEvictAnnotation(ae, evict); + ops = lazyInit(ops); + ops.add(parseEvictAnnotation(ae, evict)); } - return null; + CacheUpdate update = AnnotationUtils.getAnnotation(ae, CacheUpdate.class); + if (update != null) { + ops = lazyInit(ops); + ops.add(parseUpdateAnnotation(ae, update)); + } + CacheDefinition definition = AnnotationUtils.getAnnotation(ae, CacheDefinition.class); + if (definition != null) { + ops = lazyInit(ops); + ops.addAll(parseDefinitionAnnotation(ae, definition)); + } + return ops; } - CacheUpdateOperation parseCacheableAnnotation(AnnotatedElement ae, Cacheable ann) { - CacheUpdateOperation cuo = new CacheUpdateOperation(); + private Collection lazyInit(Collection ops) { + return (ops != null ? ops : new ArrayList(2)); + } + + CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, Cacheable ann) { + CacheableOperation cuo = new CacheableOperation(); cuo.setCacheNames(ann.value()); cuo.setCondition(ann.condition()); cuo.setKey(ann.key()); @@ -65,4 +87,40 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria return ceo; } -} + CacheOperation parseUpdateAnnotation(AnnotatedElement ae, CacheUpdate ann) { + CacheUpdateOperation cuo = new CacheUpdateOperation(); + cuo.setCacheNames(ann.value()); + cuo.setCondition(ann.condition()); + cuo.setKey(ann.key()); + cuo.setName(ae.toString()); + return cuo; + } + + Collection parseDefinitionAnnotation(AnnotatedElement ae, CacheDefinition ann) { + Collection ops = null; + + Cacheable[] cacheables = ann.cacheables(); + if (!ObjectUtils.isEmpty(cacheables)) { + ops = lazyInit(ops); + for (Cacheable cacheable : cacheables) { + ops.add(parseCacheableAnnotation(ae, cacheable)); + } + } + CacheEvict[] evicts = ann.evicts(); + if (!ObjectUtils.isEmpty(evicts)) { + ops = lazyInit(ops); + for (CacheEvict evict : evicts) { + ops.add(parseEvictAnnotation(ae, evict)); + } + } + CacheUpdate[] updates = ann.updates(); + if (!ObjectUtils.isEmpty(updates)) { + ops = lazyInit(ops); + for (CacheUpdate update : updates) { + ops.add(parseUpdateAnnotation(ae, update)); + } + } + + return ops; + } +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/cache/annotation/package-info.java b/org.springframework.context/src/main/java/org/springframework/cache/annotation/package-info.java index f61fb1e579e..e9d9a11f3bd 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/annotation/package-info.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/annotation/package-info.java @@ -1,9 +1,8 @@ /** - * * JDK 1.5+ annotation for caching demarcation. * Hooked into Spring's caching interception infrastructure - * via CacheDefinitionSource implementation. + * via CacheOperationSource implementation. * */ package org.springframework.cache.annotation; diff --git a/org.springframework.context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java b/org.springframework.context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java index ae8de0310f7..473442ceb67 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java @@ -30,7 +30,7 @@ import org.springframework.cache.annotation.AnnotationCacheOperationSource; import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cache.interceptor.CacheOperation; -import org.springframework.cache.interceptor.CacheUpdateOperation; +import org.springframework.cache.interceptor.CacheableOperation; import org.springframework.cache.interceptor.NameMatchCacheOperationSource; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -148,7 +148,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser { String name = opElement.getAttribute(METHOD_ATTRIBUTE); TypedStringValue nameHolder = new TypedStringValue(name); nameHolder.setSource(parserContext.extractSource(opElement)); - CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheUpdateOperation()); + CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation()); cacheOpeMap.put(nameHolder, op); } diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index 5387f8ba832..3d8568c9303 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -18,6 +18,8 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -51,7 +53,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * Canonical value held in cache to indicate no caching attribute was * found for this method and we don't need to look again. */ - private final static CacheOperation NULL_CACHING_ATTRIBUTE = new CacheUpdateOperation(); + private final static Collection NULL_CACHING_ATTRIBUTE = Collections.emptyList(); /** * Logger available to subclasses. @@ -65,7 +67,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera *

As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ - final Map attributeCache = new ConcurrentHashMap(); + final Map> attributeCache = new ConcurrentHashMap>(); /** @@ -76,10 +78,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * @return {@link CacheOperation} for this method, or null if the method * is not cacheable */ - public CacheOperation getCacheOperation(Method method, Class targetClass) { + public Collection getCacheOperations(Method method, Class targetClass) { // First, see if we have a cached value. Object cacheKey = getCacheKey(method, targetClass); - CacheOperation cached = this.attributeCache.get(cacheKey); + Collection cached = this.attributeCache.get(cacheKey); if (cached != null) { if (cached == NULL_CACHING_ATTRIBUTE) { return null; @@ -90,18 +92,18 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera } else { // We need to work it out. - CacheOperation cacheDef = computeCacheOperationDefinition(method, targetClass); + Collection cacheDefs = computeCacheOperationDefinition(method, targetClass); // Put it in the cache. - if (cacheDef == null) { + if (cacheDefs == null) { this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); } else { if (logger.isDebugEnabled()) { - logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheDef); + logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheDefs); } - this.attributeCache.put(cacheKey, cacheDef); + this.attributeCache.put(cacheKey, cacheDefs); } - return cacheDef; + return cacheDefs; } } @@ -117,7 +119,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera return new DefaultCacheKey(method, targetClass); } - private CacheOperation computeCacheOperationDefinition(Method method, Class targetClass) { + private Collection computeCacheOperationDefinition(Method method, Class targetClass) { // Don't allow no-public methods as required. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; @@ -130,25 +132,25 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); // First try is the method in the target class. - CacheOperation opDef = findCacheOperation(specificMethod); + Collection opDef = findCacheOperations(specificMethod); if (opDef != null) { return opDef; } // Second try is the caching operation on the target class. - opDef = findCacheOperation(specificMethod.getDeclaringClass()); + opDef = findCacheOperations(specificMethod.getDeclaringClass()); if (opDef != null) { return opDef; } if (specificMethod != method) { // Fall back is to look at the original method. - opDef = findCacheOperation(method); + opDef = findCacheOperations(method); if (opDef != null) { return opDef; } // Last fall back is the class of the original method. - return findCacheOperation(method.getDeclaringClass()); + return findCacheOperations(method.getDeclaringClass()); } return null; } @@ -161,7 +163,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * @return all caching attribute associated with this method * (or null if none) */ - protected abstract CacheOperation findCacheOperation(Method method); + protected abstract Collection findCacheOperations(Method method); /** * Subclasses need to implement this to return the caching attribute @@ -170,7 +172,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * @return all caching attribute associated with this class * (or null if none) */ - protected abstract CacheOperation findCacheOperation(Class clazz); + protected abstract Collection findCacheOperations(Class clazz); /** * Should only public methods be allowed to have caching semantics? @@ -213,5 +215,4 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera return this.method.hashCode() * 29 + (this.targetClass != null ? this.targetClass.hashCode() : 0); } } - -} +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index a090dcaed81..6c9f88fff1f 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -19,7 +19,8 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; -import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; @@ -31,6 +32,7 @@ import org.springframework.cache.CacheManager; import org.springframework.expression.EvaluationContext; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -47,7 +49,7 @@ import org.springframework.util.StringUtils; * CacheDefinitionSource is used for determining caching operation definitions. * *

A cache aspect is serializable if its CacheManager - * and CacheDefinitionSource are serializable. + * and CacheOperationSource are serializable. * * @author Costin Leau * @author Juergen Hoeller @@ -71,6 +73,7 @@ public abstract class CacheAspectSupport implements InitializingBean { private boolean initialized = false; + private static final String CACHEABLE = "cacheable", UPDATE = "cacheupdate", EVICT = "cacheevict"; /** * Set the CacheManager that this cache aspect should delegate to. @@ -92,8 +95,8 @@ public abstract class CacheAspectSupport implements InitializingBean { */ public void setCacheOperationSources(CacheOperationSource... cacheDefinitionSources) { Assert.notEmpty(cacheDefinitionSources); - this.cacheOperationSource = (cacheDefinitionSources.length > 1 ? - new CompositeCacheOperationSource(cacheDefinitionSources) : cacheDefinitionSources[0]); + this.cacheOperationSource = (cacheDefinitionSources.length > 1 ? new CompositeCacheOperationSource( + cacheDefinitionSources) : cacheDefinitionSources[0]); } /** @@ -138,7 +141,6 @@ public abstract class CacheAspectSupport implements InitializingBean { this.initialized = true; } - /** * Convenience method to return a String representation of this Method * for use in logging. Can be overridden in subclasses to provide a @@ -153,7 +155,6 @@ public abstract class CacheAspectSupport implements InitializingBean { return ClassUtils.getQualifiedMethodName(specificMethod); } - protected Collection getCaches(CacheOperation operation) { Set cacheNames = operation.getCacheNames(); Collection caches = new ArrayList(cacheNames.size()); @@ -180,107 +181,239 @@ public abstract class CacheAspectSupport implements InitializingBean { return invoker.invoke(); } - boolean log = logger.isTraceEnabled(); - // get backing class Class targetClass = AopProxyUtils.ultimateTargetClass(target); if (targetClass == null && target != null) { targetClass = target.getClass(); } - final CacheOperation cacheOp = getCacheOperationSource().getCacheOperation(method, targetClass); - - Object retVal = null; + final Collection cacheOp = getCacheOperationSource().getCacheOperations(method, targetClass); // analyze caching information - if (cacheOp != null) { - CacheOperationContext context = getOperationContext(cacheOp, method, args, target, targetClass); - Collection caches = context.getCaches(); + if (!CollectionUtils.isEmpty(cacheOp)) { + Map> ops = createOperationContext(cacheOp, method, args, target, + targetClass); - if (context.hasConditionPassed()) { - // check operation - if (cacheOp instanceof CacheUpdateOperation) { - Object key = context.generateKey(); - if (log) { - logger.trace("Computed cache key " + key + " for definition " + cacheOp); - } - if (key == null) { - throw new IllegalArgumentException( - "Null key returned for cache definition (maybe you are using named params on classes without debug info?) " - + cacheOp); - } + // start with evictions + inspectCacheEvicts(ops.get(EVICT)); - // for each cache - boolean cacheHit = false; + // follow up with cacheable + CacheStatus status = inspectCacheables(ops.get(CACHEABLE)); - for (Iterator iterator = caches.iterator(); iterator.hasNext() && !cacheHit;) { - Cache cache = iterator.next(); - Cache.ValueWrapper wrapper = cache.get(key); - if (wrapper != null) { - cacheHit = true; - retVal = wrapper.get(); - } - } + Object retVal = null; + Map updates = inspectCacheUpdates(ops.get(UPDATE)); - if (!cacheHit) { - if (log) { - logger.trace("Key " + key + " NOT found in cache(s), invoking cached target method " - + method); - } - retVal = invoker.invoke(); - // update all caches - for (Cache cache : caches) { - cache.put(key, retVal); - } - } - else { - if (log) { - logger.trace("Key " + key + " found in cache, returning value " + retVal); - } - } + if (status != null) { + if (status.updateRequired) { + updates.putAll(status.cUpdates); } + // return cached object + else { + return status.retVal; + } + } - if (cacheOp instanceof CacheEvictOperation) { - CacheEvictOperation evictOp = (CacheEvictOperation) cacheOp; + retVal = invoker.invoke(); + + if (!updates.isEmpty()) { + update(updates, retVal); + } + + return retVal; + } + + return invoker.invoke(); + } + + private void inspectCacheEvicts(Collection evictions) { + + if (!evictions.isEmpty()) { + + boolean log = logger.isTraceEnabled(); + + for (CacheOperationContext context : evictions) { + if (context.isConditionPassing()) { + CacheEvictOperation evictOp = (CacheEvictOperation) context.operation; // for each cache // lazy key initialization Object key = null; - for (Cache cache : caches) { - // flush the cache (ignore arguments) + for (Cache cache : context.getCaches()) { + // cache-wide flush if (evictOp.isCacheWide()) { cache.clear(); if (log) { - logger.trace("Invalidating entire cache for definition " + cacheOp + - " on method " + method); + logger.trace("Invalidating entire cache for definition " + evictOp + " on method " + context.method); } - } - else { + } else { // check key if (key == null) { key = context.generateKey(); } if (log) { - logger.trace("Invalidating cache key " + key + " for definition " + cacheOp - + " on method " + method); + logger.trace("Invalidating cache key " + key + " for definition " + evictOp + " on method " + context.method); } cache.evict(key); } } - retVal = invoker.invoke(); } - return retVal; + else { + if (log) { + logger.trace("Cache condition failed on method " + context.method + " for definition " + context.operation); + } + } } - else { - if (log) { - logger.trace("Cache condition failed on method " + method + " for definition " + cacheOp); + } + } + + private CacheStatus inspectCacheables(Collection cacheables) { + Map cUpdates = new LinkedHashMap( + cacheables.size()); + + boolean updateRequire = false; + Object retVal = null; + + if (!cacheables.isEmpty()) { + boolean log = logger.isTraceEnabled(); + boolean atLeastOnePassed = false; + + for (CacheOperationContext context : cacheables) { + if (context.isConditionPassing()) { + atLeastOnePassed = true; + Object key = context.generateKey(); + + if (log) { + logger.trace("Computed cache key " + key + " for definition " + context.operation); + } + if (key == null) { + throw new IllegalArgumentException( + "Null key returned for cache definition (maybe you are using named params on classes without debug info?) " + + context.operation); + } + + // add op/key (in case an update is discovered later on) + cUpdates.put(context, key); + + boolean localCacheHit = false; + + // check whether the cache needs to be inspected or not (the method will be invoked anyway) + if (!updateRequire) { + for (Cache cache : context.getCaches()) { + Cache.ValueWrapper wrapper = cache.get(key); + if (wrapper != null) { + retVal = wrapper.get(); + localCacheHit = true; + break; + } + } + } + + if (!localCacheHit) { + updateRequire = true; + } + } + else { + if (log) { + logger.trace("Cache condition failed on method " + context.method + " for definition " + context.operation); + } + } + } + + // return a status only if at least on cacheable matched + if (atLeastOnePassed) { + return new CacheStatus(cUpdates, updateRequire, retVal); + } + } + + return null; + } + + private static class CacheStatus { + // caches/key + final Map cUpdates; + final boolean updateRequired; + final Object retVal; + + CacheStatus(Map cUpdates, boolean updateRequired, Object retVal) { + this.cUpdates = cUpdates; + this.updateRequired = updateRequired; + this.retVal = retVal; + } + } + + private Map inspectCacheUpdates(Collection updates) { + + Map cUpdates = new LinkedHashMap(updates.size()); + + if (!updates.isEmpty()) { + boolean log = logger.isTraceEnabled(); + + for (CacheOperationContext context : updates) { + if (context.isConditionPassing()) { + + Object key = context.generateKey(); + + if (log) { + logger.trace("Computed cache key " + key + " for definition " + context.operation); + } + if (key == null) { + throw new IllegalArgumentException( + "Null key returned for cache definition (maybe you are using named params on classes without debug info?) " + + context.operation); + } + + // add op/key (in case an update is discovered later on) + cUpdates.put(context, key); + } + else { + if (log) { + logger.trace("Cache condition failed on method " + context.method + " for definition " + context.operation); + } } } } - return invoker.invoke(); + return cUpdates; } + private void update(Map updates, Object retVal) { + for (Map.Entry entry : updates.entrySet()) { + for (Cache cache : entry.getKey().getCaches()) { + cache.put(entry.getValue(), retVal); + } + } + } + + private Map> createOperationContext(Collection cacheOp, + Method method, Object[] args, Object target, Class targetClass) { + Map> map = new LinkedHashMap>(3); + + Collection cacheables = new ArrayList(); + Collection evicts = new ArrayList(); + Collection updates = new ArrayList(); + + for (CacheOperation cacheOperation : cacheOp) { + CacheOperationContext opContext = getOperationContext(cacheOperation, method, args, target, targetClass); + + if (cacheOperation instanceof CacheableOperation) { + cacheables.add(opContext); + } + + if (cacheOperation instanceof CacheEvictOperation) { + evicts.add(opContext); + } + + if (cacheOperation instanceof CacheUpdateOperation) { + updates.add(opContext); + } + } + + map.put(CACHEABLE, cacheables); + map.put(EVICT, evicts); + map.put(UPDATE, updates); + + return map; + } protected class CacheOperationContext { @@ -308,7 +441,7 @@ public abstract class CacheAspectSupport implements InitializingBean { this.evalContext = evaluator.createEvaluationContext(caches, method, args, target, targetClass); } - protected boolean hasConditionPassed() { + protected boolean isConditionPassing() { if (StringUtils.hasText(this.operation.getCondition())) { return evaluator.condition(this.operation.getCondition(), this.method, this.evalContext); } @@ -330,5 +463,4 @@ public abstract class CacheAspectSupport implements InitializingBean { return this.caches; } } - -} +} \ No newline at end of file diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationEditor.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationEditor.java index fb6b67dbf1c..97da30b2ad0 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationEditor.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationEditor.java @@ -57,7 +57,7 @@ public class CacheOperationEditor extends PropertyEditorSupport { CacheOperation op; if ("cacheable".contains(tokens[0])) { - op = new CacheUpdateOperation(); + op = new CacheableOperation(); } else if ("evict".contains(tokens[0])) { diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 0d464275d9f..7d45ec504df 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -17,6 +17,7 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; +import java.util.Collection; /** * Interface used by CacheInterceptor. Implementations know @@ -30,13 +31,13 @@ public interface CacheOperationSource { /** * Return the cache operation definition for this method, - * or null if the method is not cacheable. + * or null if the method contains no "cacheable" annotations. * @param method the method to introspect * @param targetClass the target class (may be null, * in which case the declaring class of the method must be used) * @return {@link CacheOperation} the matching cache operation, * or null if none found */ - CacheOperation getCacheOperation(Method method, Class targetClass); + Collection getCacheOperations(Method method, Class targetClass); } diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index cfc84160471..9f357431100 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -20,6 +20,7 @@ import java.io.Serializable; import java.lang.reflect.Method; import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** @@ -34,7 +35,7 @@ abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut public boolean matches(Method method, Class targetClass) { CacheOperationSource cas = getCacheOperationSource(); - return (cas == null || cas.getCacheOperation(method, targetClass) != null); + return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass))); } @Override diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheUpdateOperation.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheUpdateOperation.java index 7229b1a3ae4..a2cb6c93ea9 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheUpdateOperation.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheUpdateOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2011 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. diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java new file mode 100644 index 00000000000..a00eeff3044 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2011 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cache.interceptor; + +/** + * Class describing a cache 'cacheable' operation. + * + * @author Costin Leau + * @since 3.1 + */ +public class CacheableOperation extends CacheOperation { + +} diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java index 5215f551501..2ee5d23f4b9 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java @@ -18,6 +18,8 @@ package org.springframework.cache.interceptor; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; import org.springframework.util.Assert; @@ -33,7 +35,6 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri private final CacheOperationSource[] cacheOperationSources; - /** * Create a new CompositeCacheOperationSource for the given sources. * @param cacheOperationSources the CacheOperationSource instances to combine @@ -43,7 +44,6 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri this.cacheOperationSources = cacheOperationSources; } - /** * Return the CacheOperationSource instances that this CompositeCachingDefinitionSource combines. */ @@ -51,15 +51,19 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri return this.cacheOperationSources; } + public Collection getCacheOperations(Method method, Class targetClass) { + Collection ops = null; - public CacheOperation getCacheOperation(Method method, Class targetClass) { for (CacheOperationSource source : this.cacheOperationSources) { - CacheOperation definition = source.getCacheOperation(method, targetClass); - if (definition != null) { - return definition; + Collection cacheOperations = source.getCacheOperations(method, targetClass); + if (cacheOperations != null) { + if (ops == null) { + ops = new ArrayList(); + } + + ops.addAll(cacheOperations); } } - return null; + return ops; } - } diff --git a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java index 6c4df9fff89..aae17502ec6 100644 --- a/org.springframework.context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java +++ b/org.springframework.context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java @@ -18,6 +18,8 @@ package org.springframework.cache.interceptor; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; import java.util.Enumeration; import java.util.LinkedHashMap; import java.util.Map; @@ -43,7 +45,7 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri protected static final Log logger = LogFactory.getLog(NameMatchCacheOperationSource.class); /** Keys are method names; values are TransactionAttributes */ - private Map nameMap = new LinkedHashMap(); + private Map> nameMap = new LinkedHashMap>(); /** * Set a name/attribute map, consisting of method names @@ -88,13 +90,13 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri if (logger.isDebugEnabled()) { logger.debug("Adding method [" + methodName + "] with cache operation [" + operation + "]"); } - this.nameMap.put(methodName, operation); + this.nameMap.put(methodName, Collections.singleton(operation)); } - public CacheOperation getCacheOperation(Method method, Class targetClass) { + public Collection getCacheOperations(Method method, Class targetClass) { // look for direct name match String methodName = method.getName(); - CacheOperation attr = this.nameMap.get(methodName); + Collection attr = this.nameMap.get(methodName); if (attr == null) { // Look for most specific name match. diff --git a/org.springframework.context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java b/org.springframework.context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java index cad5e470402..75f11db7973 100644 --- a/org.springframework.context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java +++ b/org.springframework.context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java @@ -176,6 +176,31 @@ public abstract class AbstractAnnotationTests { assertSame(r1, service.cache(null)); } + public void testCacheUpdate(CacheableService service) { + Object o = new Object(); + Cache cache = cm.getCache("default"); + assertNull(cache.get(o)); + Object r1 = service.update(o); + assertSame(r1, cache.get(o).get()); + + o = new Object(); + assertNull(cache.get(o)); + Object r2 = service.update(o); + assertSame(r2, cache.get(o).get()); + } + + public void testConditionalCacheUpdate(CacheableService service) { + Integer one = Integer.valueOf(1); + Integer three = Integer.valueOf(3); + + Cache cache = cm.getCache("default"); + assertEquals(one, Integer.valueOf(service.conditionalUpdate(one).toString())); + assertNull(cache.get(one)); + + assertEquals(three, Integer.valueOf(service.conditionalUpdate(three).toString())); + assertEquals(three, Integer.valueOf(cache.get(three).get().toString())); + } + @Test public void testCacheable() throws Exception { testCacheable(cs); @@ -284,4 +309,24 @@ public abstract class AbstractAnnotationTests { public void testClassUncheckedException() throws Exception { testUncheckedThrowable(ccs); } + + @Test + public void testUpdate() { + testCacheUpdate(cs); + } + + @Test + public void testClassUpdate() { + testCacheUpdate(ccs); + } + + @Test + public void testConditionalUpdate() { + testConditionalCacheUpdate(cs); + } + + @Test + public void testClassConditionalUpdate() { + testConditionalCacheUpdate(ccs); + } } \ No newline at end of file diff --git a/org.springframework.context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/org.springframework.context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index 66961cc42fe..503b9a52ec8 100644 --- a/org.springframework.context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java +++ b/org.springframework.context/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -19,6 +19,7 @@ package org.springframework.cache.config; import java.util.concurrent.atomic.AtomicLong; import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CacheUpdate; import org.springframework.cache.annotation.Cacheable; /** @@ -61,6 +62,16 @@ public class AnnotatedClassCacheableService implements CacheableService { return counter.getAndIncrement(); } + @CacheUpdate("default") + public Object update(Object arg1) { + return counter.getAndIncrement(); + } + + @CacheUpdate(value = "default", condition = "#arg.equals(3)") + public Object conditionalUpdate(Object arg) { + return arg; + } + public Object nullValue(Object arg1) { nullInvocations.incrementAndGet(); return null; diff --git a/org.springframework.context/src/test/java/org/springframework/cache/config/CacheableService.java b/org.springframework.context/src/test/java/org/springframework/cache/config/CacheableService.java index 98f62525470..e145ea3e613 100644 --- a/org.springframework.context/src/test/java/org/springframework/cache/config/CacheableService.java +++ b/org.springframework.context/src/test/java/org/springframework/cache/config/CacheableService.java @@ -38,6 +38,10 @@ public interface CacheableService { T nullValue(Object arg1); + T update(Object arg1); + + T conditionalUpdate(Object arg2); + Number nullInvocations(); T rootVars(Object arg1); diff --git a/org.springframework.context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/org.springframework.context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java index d02248f9301..1d796204b1f 100644 --- a/org.springframework.context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java +++ b/org.springframework.context/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -19,6 +19,7 @@ package org.springframework.cache.config; import java.util.concurrent.atomic.AtomicLong; import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CacheUpdate; import org.springframework.cache.annotation.Cacheable; /** @@ -64,6 +65,16 @@ public class DefaultCacheableService implements CacheableService { return counter.getAndIncrement(); } + @CacheUpdate("default") + public Long update(Object arg1) { + return counter.getAndIncrement(); + } + + @CacheUpdate(value = "default", condition = "#arg.equals(3)") + public Long conditionalUpdate(Object arg) { + return Long.valueOf(arg.toString()); + } + @Cacheable("default") public Long nullValue(Object arg1) { nullInvocations.incrementAndGet();