+ introduced @CacheUpdate annotation

+ introduced @CacheDefinition annotation
+ introduced meta-annotation to allow multiple @Cache annotations
SPR-7833
SPR-8082
This commit is contained in:
Costin Leau 2011-11-09 10:00:44 +00:00
parent c3f0f31243
commit eddb0ac3be
21 changed files with 540 additions and 136 deletions

View File

@ -19,6 +19,8 @@ package org.springframework.cache.annotation;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
@ -32,7 +34,7 @@ import org.springframework.util.Assert;
* Implementation of the {@link org.springframework.cache.interceptor.CacheOperationSource} * Implementation of the {@link org.springframework.cache.interceptor.CacheOperationSource}
* interface for working with caching metadata in JDK 1.5+ annotation format. * interface for working with caching metadata in JDK 1.5+ annotation format.
* *
* <p>This class reads Spring's JDK 1.5+ {@link Cacheable} and {@link CacheEvict} * <p>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. * annotations and exposes corresponding caching operation definition to Spring's cache infrastructure.
* This class may also serve as base class for a custom CacheOperationSource. * This class may also serve as base class for a custom CacheOperationSource.
* *
@ -83,13 +85,13 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
@Override @Override
protected CacheOperation findCacheOperation(Class<?> clazz) { protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
return determineCacheOperation(clazz); return determineCacheOperations(clazz);
} }
@Override @Override
protected CacheOperation findCacheOperation(Method method) { protected Collection<CacheOperation> findCacheOperations(Method method) {
return determineCacheOperation(method); return determineCacheOperations(method);
} }
/** /**
@ -103,14 +105,19 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
* @return CacheOperation the configured caching operation, * @return CacheOperation the configured caching operation,
* or <code>null</code> if none was found * or <code>null</code> if none was found
*/ */
protected CacheOperation determineCacheOperation(AnnotatedElement ae) { protected Collection<CacheOperation> determineCacheOperations(AnnotatedElement ae) {
Collection<CacheOperation> ops = null;
for (CacheAnnotationParser annotationParser : this.annotationParsers) { for (CacheAnnotationParser annotationParser : this.annotationParsers) {
CacheOperation attr = annotationParser.parseCacheAnnotation(ae); Collection<CacheOperation> annOps = annotationParser.parseCacheAnnotations(ae);
if (attr != null) { if (annOps != null) {
return attr; if (ops == null) {
ops = new ArrayList<CacheOperation>();
}
ops.addAll(annOps);
} }
} }
return null; return ops;
} }
/** /**
@ -120,5 +127,4 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
protected boolean allowPublicMethodsOnly() { protected boolean allowPublicMethodsOnly() {
return this.publicMethodsOnly; return this.publicMethodsOnly;
} }
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.cache.annotation; package org.springframework.cache.annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.util.Collection;
import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CacheOperation;
@ -25,7 +26,7 @@ import org.springframework.cache.interceptor.CacheOperation;
* Strategy interface for parsing known caching annotation types. * Strategy interface for parsing known caching annotation types.
* {@link AnnotationCacheDefinitionSource} delegates to such * {@link AnnotationCacheDefinitionSource} delegates to such
* parsers for supporting specific annotation types such as Spring's own * 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 * @author Costin Leau
* @since 3.1 * @since 3.1
@ -43,6 +44,5 @@ public interface CacheAnnotationParser {
* or <code>null</code> if none was found * or <code>null</code> if none was found
* @see AnnotationCacheOperationSource#determineCacheOperation * @see AnnotationCacheOperationSource#determineCacheOperation
*/ */
CacheOperation parseCacheAnnotation(AnnotatedElement ae); Collection<CacheOperation> parseCacheAnnotations(AnnotatedElement ae);
} }

View File

@ -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();
}

View File

@ -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.
* <p>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.
* <p>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.
* <p>Default is "", meaning the method result is always cached.
*/
String condition() default "";
}

View File

@ -56,5 +56,4 @@ public @interface Cacheable {
* <p>Default is "", meaning the method is always cached. * <p>Default is "", meaning the method is always cached.
*/ */
String condition() default ""; String condition() default "";
} }

View File

@ -18,14 +18,18 @@ package org.springframework.cache.annotation;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheEvictOperation;
import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CacheUpdateOperation; import org.springframework.cache.interceptor.CacheUpdateOperation;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.core.annotation.AnnotationUtils; 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 Costin Leau
* @author Juergen Hoeller * @author Juergen Hoeller
@ -34,20 +38,38 @@ import org.springframework.core.annotation.AnnotationUtils;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable { public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
public CacheOperation parseCacheAnnotation(AnnotatedElement ae) { public Collection<CacheOperation> parseCacheAnnotations(AnnotatedElement ae) {
Cacheable update = AnnotationUtils.getAnnotation(ae, Cacheable.class); Collection<CacheOperation> ops = null;
if (update != null) {
return parseCacheableAnnotation(ae, update); 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); CacheEvict evict = AnnotationUtils.getAnnotation(ae, CacheEvict.class);
if (evict != null) { 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) { private Collection<CacheOperation> lazyInit(Collection<CacheOperation> ops) {
CacheUpdateOperation cuo = new CacheUpdateOperation(); return (ops != null ? ops : new ArrayList<CacheOperation>(2));
}
CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, Cacheable ann) {
CacheableOperation cuo = new CacheableOperation();
cuo.setCacheNames(ann.value()); cuo.setCacheNames(ann.value());
cuo.setCondition(ann.condition()); cuo.setCondition(ann.condition());
cuo.setKey(ann.key()); cuo.setKey(ann.key());
@ -65,4 +87,40 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return ceo; 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<CacheOperation> parseDefinitionAnnotation(AnnotatedElement ae, CacheDefinition ann) {
Collection<CacheOperation> 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;
}
}

View File

@ -1,9 +1,8 @@
/** /**
*
* JDK 1.5+ annotation for caching demarcation. * JDK 1.5+ annotation for caching demarcation.
* Hooked into Spring's caching interception infrastructure * Hooked into Spring's caching interception infrastructure
* via CacheDefinitionSource implementation. * via CacheOperationSource implementation.
* *
*/ */
package org.springframework.cache.annotation; package org.springframework.cache.annotation;

View File

@ -30,7 +30,7 @@ import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheEvictOperation;
import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.interceptor.CacheOperation; 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.cache.interceptor.NameMatchCacheOperationSource;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils; import org.springframework.util.xml.DomUtils;
@ -148,7 +148,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
String name = opElement.getAttribute(METHOD_ATTRIBUTE); String name = opElement.getAttribute(METHOD_ATTRIBUTE);
TypedStringValue nameHolder = new TypedStringValue(name); TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement)); 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); cacheOpeMap.put(nameHolder, op);
} }

View File

@ -18,6 +18,8 @@ package org.springframework.cache.interceptor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; 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 * Canonical value held in cache to indicate no caching attribute was
* found for this method and we don't need to look again. * 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<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();
/** /**
* Logger available to subclasses. * Logger available to subclasses.
@ -65,7 +67,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
* <p>As this base class is not marked Serializable, the cache will be recreated * <p>As this base class is not marked Serializable, the cache will be recreated
* after serialization - provided that the concrete subclass is Serializable. * after serialization - provided that the concrete subclass is Serializable.
*/ */
final Map<Object, CacheOperation> attributeCache = new ConcurrentHashMap<Object, CacheOperation>(); final Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<Object, Collection<CacheOperation>>();
/** /**
@ -76,10 +78,10 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
* @return {@link CacheOperation} for this method, or <code>null</code> if the method * @return {@link CacheOperation} for this method, or <code>null</code> if the method
* is not cacheable * is not cacheable
*/ */
public CacheOperation getCacheOperation(Method method, Class<?> targetClass) { public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
// First, see if we have a cached value. // First, see if we have a cached value.
Object cacheKey = getCacheKey(method, targetClass); Object cacheKey = getCacheKey(method, targetClass);
CacheOperation cached = this.attributeCache.get(cacheKey); Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
if (cached != null) { if (cached != null) {
if (cached == NULL_CACHING_ATTRIBUTE) { if (cached == NULL_CACHING_ATTRIBUTE) {
return null; return null;
@ -90,18 +92,18 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
} }
else { else {
// We need to work it out. // We need to work it out.
CacheOperation cacheDef = computeCacheOperationDefinition(method, targetClass); Collection<CacheOperation> cacheDefs = computeCacheOperationDefinition(method, targetClass);
// Put it in the cache. // Put it in the cache.
if (cacheDef == null) { if (cacheDefs == null) {
this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE); this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
} }
else { else {
if (logger.isDebugEnabled()) { 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); return new DefaultCacheKey(method, targetClass);
} }
private CacheOperation computeCacheOperationDefinition(Method method, Class<?> targetClass) { private Collection<CacheOperation> computeCacheOperationDefinition(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required. // Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null; return null;
@ -130,25 +132,25 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// First try is the method in the target class. // First try is the method in the target class.
CacheOperation opDef = findCacheOperation(specificMethod); Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
if (opDef != null) { if (opDef != null) {
return opDef; return opDef;
} }
// Second try is the caching operation on the target class. // Second try is the caching operation on the target class.
opDef = findCacheOperation(specificMethod.getDeclaringClass()); opDef = findCacheOperations(specificMethod.getDeclaringClass());
if (opDef != null) { if (opDef != null) {
return opDef; return opDef;
} }
if (specificMethod != method) { if (specificMethod != method) {
// Fall back is to look at the original method. // Fall back is to look at the original method.
opDef = findCacheOperation(method); opDef = findCacheOperations(method);
if (opDef != null) { if (opDef != null) {
return opDef; return opDef;
} }
// Last fall back is the class of the original method. // Last fall back is the class of the original method.
return findCacheOperation(method.getDeclaringClass()); return findCacheOperations(method.getDeclaringClass());
} }
return null; return null;
} }
@ -161,7 +163,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
* @return all caching attribute associated with this method * @return all caching attribute associated with this method
* (or <code>null</code> if none) * (or <code>null</code> if none)
*/ */
protected abstract CacheOperation findCacheOperation(Method method); protected abstract Collection<CacheOperation> findCacheOperations(Method method);
/** /**
* Subclasses need to implement this to return the caching attribute * 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 * @return all caching attribute associated with this class
* (or <code>null</code> if none) * (or <code>null</code> if none)
*/ */
protected abstract CacheOperation findCacheOperation(Class<?> clazz); protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);
/** /**
* Should only public methods be allowed to have caching semantics? * 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); return this.method.hashCode() * 29 + (this.targetClass != null ? this.targetClass.hashCode() : 0);
} }
} }
}
}

View File

@ -19,7 +19,8 @@ package org.springframework.cache.interceptor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -31,6 +32,7 @@ import org.springframework.cache.CacheManager;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -47,7 +49,7 @@ import org.springframework.util.StringUtils;
* <code>CacheDefinitionSource</code> is used for determining caching operation definitions. * <code>CacheDefinitionSource</code> is used for determining caching operation definitions.
* *
* <p>A cache aspect is serializable if its <code>CacheManager</code> * <p>A cache aspect is serializable if its <code>CacheManager</code>
* and <code>CacheDefinitionSource</code> are serializable. * and <code>CacheOperationSource</code> are serializable.
* *
* @author Costin Leau * @author Costin Leau
* @author Juergen Hoeller * @author Juergen Hoeller
@ -71,6 +73,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
private boolean initialized = false; private boolean initialized = false;
private static final String CACHEABLE = "cacheable", UPDATE = "cacheupdate", EVICT = "cacheevict";
/** /**
* Set the CacheManager that this cache aspect should delegate to. * 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) { public void setCacheOperationSources(CacheOperationSource... cacheDefinitionSources) {
Assert.notEmpty(cacheDefinitionSources); Assert.notEmpty(cacheDefinitionSources);
this.cacheOperationSource = (cacheDefinitionSources.length > 1 ? this.cacheOperationSource = (cacheDefinitionSources.length > 1 ? new CompositeCacheOperationSource(
new CompositeCacheOperationSource(cacheDefinitionSources) : cacheDefinitionSources[0]); cacheDefinitionSources) : cacheDefinitionSources[0]);
} }
/** /**
@ -138,7 +141,6 @@ public abstract class CacheAspectSupport implements InitializingBean {
this.initialized = true; this.initialized = true;
} }
/** /**
* Convenience method to return a String representation of this Method * Convenience method to return a String representation of this Method
* for use in logging. Can be overridden in subclasses to provide a * 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); return ClassUtils.getQualifiedMethodName(specificMethod);
} }
protected Collection<Cache> getCaches(CacheOperation operation) { protected Collection<Cache> getCaches(CacheOperation operation) {
Set<String> cacheNames = operation.getCacheNames(); Set<String> cacheNames = operation.getCacheNames();
Collection<Cache> caches = new ArrayList<Cache>(cacheNames.size()); Collection<Cache> caches = new ArrayList<Cache>(cacheNames.size());
@ -180,107 +181,239 @@ public abstract class CacheAspectSupport implements InitializingBean {
return invoker.invoke(); return invoker.invoke();
} }
boolean log = logger.isTraceEnabled();
// get backing class // get backing class
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target); Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
if (targetClass == null && target != null) { if (targetClass == null && target != null) {
targetClass = target.getClass(); targetClass = target.getClass();
} }
final CacheOperation cacheOp = getCacheOperationSource().getCacheOperation(method, targetClass); final Collection<CacheOperation> cacheOp = getCacheOperationSource().getCacheOperations(method, targetClass);
Object retVal = null;
// analyze caching information // analyze caching information
if (cacheOp != null) { if (!CollectionUtils.isEmpty(cacheOp)) {
CacheOperationContext context = getOperationContext(cacheOp, method, args, target, targetClass); Map<String, Collection<CacheOperationContext>> ops = createOperationContext(cacheOp, method, args, target,
Collection<Cache> caches = context.getCaches(); targetClass);
if (context.hasConditionPassed()) { // start with evictions
// check operation inspectCacheEvicts(ops.get(EVICT));
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);
}
// for each cache // follow up with cacheable
boolean cacheHit = false; CacheStatus status = inspectCacheables(ops.get(CACHEABLE));
for (Iterator<Cache> iterator = caches.iterator(); iterator.hasNext() && !cacheHit;) { Object retVal = null;
Cache cache = iterator.next(); Map<CacheOperationContext, Object> updates = inspectCacheUpdates(ops.get(UPDATE));
Cache.ValueWrapper wrapper = cache.get(key);
if (wrapper != null) {
cacheHit = true;
retVal = wrapper.get();
}
}
if (!cacheHit) { if (status != null) {
if (log) { if (status.updateRequired) {
logger.trace("Key " + key + " NOT found in cache(s), invoking cached target method " updates.putAll(status.cUpdates);
+ 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);
}
}
} }
// return cached object
else {
return status.retVal;
}
}
if (cacheOp instanceof CacheEvictOperation) { retVal = invoker.invoke();
CacheEvictOperation evictOp = (CacheEvictOperation) cacheOp;
if (!updates.isEmpty()) {
update(updates, retVal);
}
return retVal;
}
return invoker.invoke();
}
private void inspectCacheEvicts(Collection<CacheOperationContext> evictions) {
if (!evictions.isEmpty()) {
boolean log = logger.isTraceEnabled();
for (CacheOperationContext context : evictions) {
if (context.isConditionPassing()) {
CacheEvictOperation evictOp = (CacheEvictOperation) context.operation;
// for each cache // for each cache
// lazy key initialization // lazy key initialization
Object key = null; Object key = null;
for (Cache cache : caches) { for (Cache cache : context.getCaches()) {
// flush the cache (ignore arguments) // cache-wide flush
if (evictOp.isCacheWide()) { if (evictOp.isCacheWide()) {
cache.clear(); cache.clear();
if (log) { if (log) {
logger.trace("Invalidating entire cache for definition " + cacheOp + logger.trace("Invalidating entire cache for definition " + evictOp + " on method " + context.method);
" on method " + method);
} }
} } else {
else {
// check key // check key
if (key == null) { if (key == null) {
key = context.generateKey(); key = context.generateKey();
} }
if (log) { if (log) {
logger.trace("Invalidating cache key " + key + " for definition " + cacheOp logger.trace("Invalidating cache key " + key + " for definition " + evictOp + " on method " + context.method);
+ " on method " + method);
} }
cache.evict(key); 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<CacheOperationContext> cacheables) {
Map<CacheOperationContext, Object> cUpdates = new LinkedHashMap<CacheOperationContext, Object>(
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<CacheOperationContext, Object> cUpdates;
final boolean updateRequired;
final Object retVal;
CacheStatus(Map<CacheOperationContext, Object> cUpdates, boolean updateRequired, Object retVal) {
this.cUpdates = cUpdates;
this.updateRequired = updateRequired;
this.retVal = retVal;
}
}
private Map<CacheOperationContext, Object> inspectCacheUpdates(Collection<CacheOperationContext> updates) {
Map<CacheOperationContext, Object> cUpdates = new LinkedHashMap<CacheOperationContext, Object>(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<CacheOperationContext, Object> updates, Object retVal) {
for (Map.Entry<CacheOperationContext, Object> entry : updates.entrySet()) {
for (Cache cache : entry.getKey().getCaches()) {
cache.put(entry.getValue(), retVal);
}
}
}
private Map<String, Collection<CacheOperationContext>> createOperationContext(Collection<CacheOperation> cacheOp,
Method method, Object[] args, Object target, Class<?> targetClass) {
Map<String, Collection<CacheOperationContext>> map = new LinkedHashMap<String, Collection<CacheOperationContext>>(3);
Collection<CacheOperationContext> cacheables = new ArrayList<CacheOperationContext>();
Collection<CacheOperationContext> evicts = new ArrayList<CacheOperationContext>();
Collection<CacheOperationContext> updates = new ArrayList<CacheOperationContext>();
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 { protected class CacheOperationContext {
@ -308,7 +441,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
this.evalContext = evaluator.createEvaluationContext(caches, method, args, target, targetClass); this.evalContext = evaluator.createEvaluationContext(caches, method, args, target, targetClass);
} }
protected boolean hasConditionPassed() { protected boolean isConditionPassing() {
if (StringUtils.hasText(this.operation.getCondition())) { if (StringUtils.hasText(this.operation.getCondition())) {
return evaluator.condition(this.operation.getCondition(), this.method, this.evalContext); return evaluator.condition(this.operation.getCondition(), this.method, this.evalContext);
} }
@ -330,5 +463,4 @@ public abstract class CacheAspectSupport implements InitializingBean {
return this.caches; return this.caches;
} }
} }
}
}

View File

@ -57,7 +57,7 @@ public class CacheOperationEditor extends PropertyEditorSupport {
CacheOperation op; CacheOperation op;
if ("cacheable".contains(tokens[0])) { if ("cacheable".contains(tokens[0])) {
op = new CacheUpdateOperation(); op = new CacheableOperation();
} }
else if ("evict".contains(tokens[0])) { else if ("evict".contains(tokens[0])) {

View File

@ -17,6 +17,7 @@
package org.springframework.cache.interceptor; package org.springframework.cache.interceptor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection;
/** /**
* Interface used by CacheInterceptor. Implementations know * Interface used by CacheInterceptor. Implementations know
@ -30,13 +31,13 @@ public interface CacheOperationSource {
/** /**
* Return the cache operation definition for this method, * Return the cache operation definition for this method,
* or <code>null</code> if the method is not cacheable. * or <code>null</code> if the method contains no "cacheable" annotations.
* @param method the method to introspect * @param method the method to introspect
* @param targetClass the target class (may be <code>null</code>, * @param targetClass the target class (may be <code>null</code>,
* in which case the declaring class of the method must be used) * in which case the declaring class of the method must be used)
* @return {@link CacheOperation} the matching cache operation, * @return {@link CacheOperation} the matching cache operation,
* or <code>null</code> if none found * or <code>null</code> if none found
*/ */
CacheOperation getCacheOperation(Method method, Class<?> targetClass); Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass);
} }

View File

@ -20,6 +20,7 @@ import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@ -34,7 +35,7 @@ abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut
public boolean matches(Method method, Class<?> targetClass) { public boolean matches(Method method, Class<?> targetClass) {
CacheOperationSource cas = getCacheOperationSource(); CacheOperationSource cas = getCacheOperationSource();
return (cas == null || cas.getCacheOperation(method, targetClass) != null); return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
} }
@Override @Override

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -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 {
}

View File

@ -18,6 +18,8 @@ package org.springframework.cache.interceptor;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -33,7 +35,6 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri
private final CacheOperationSource[] cacheOperationSources; private final CacheOperationSource[] cacheOperationSources;
/** /**
* Create a new CompositeCacheOperationSource for the given sources. * Create a new CompositeCacheOperationSource for the given sources.
* @param cacheOperationSources the CacheOperationSource instances to combine * @param cacheOperationSources the CacheOperationSource instances to combine
@ -43,7 +44,6 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri
this.cacheOperationSources = cacheOperationSources; this.cacheOperationSources = cacheOperationSources;
} }
/** /**
* Return the CacheOperationSource instances that this CompositeCachingDefinitionSource combines. * Return the CacheOperationSource instances that this CompositeCachingDefinitionSource combines.
*/ */
@ -51,15 +51,19 @@ public class CompositeCacheOperationSource implements CacheOperationSource, Seri
return this.cacheOperationSources; return this.cacheOperationSources;
} }
public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
Collection<CacheOperation> ops = null;
public CacheOperation getCacheOperation(Method method, Class<?> targetClass) {
for (CacheOperationSource source : this.cacheOperationSources) { for (CacheOperationSource source : this.cacheOperationSources) {
CacheOperation definition = source.getCacheOperation(method, targetClass); Collection<CacheOperation> cacheOperations = source.getCacheOperations(method, targetClass);
if (definition != null) { if (cacheOperations != null) {
return definition; if (ops == null) {
ops = new ArrayList<CacheOperation>();
}
ops.addAll(cacheOperations);
} }
} }
return null; return ops;
} }
} }

View File

@ -18,6 +18,8 @@ package org.springframework.cache.interceptor;
import java.io.Serializable; import java.io.Serializable;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
@ -43,7 +45,7 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri
protected static final Log logger = LogFactory.getLog(NameMatchCacheOperationSource.class); protected static final Log logger = LogFactory.getLog(NameMatchCacheOperationSource.class);
/** Keys are method names; values are TransactionAttributes */ /** Keys are method names; values are TransactionAttributes */
private Map<String, CacheOperation> nameMap = new LinkedHashMap<String, CacheOperation>(); private Map<String, Collection<CacheOperation>> nameMap = new LinkedHashMap<String, Collection<CacheOperation>>();
/** /**
* Set a name/attribute map, consisting of method names * Set a name/attribute map, consisting of method names
@ -88,13 +90,13 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Adding method [" + methodName + "] with cache operation [" + operation + "]"); 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<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
// look for direct name match // look for direct name match
String methodName = method.getName(); String methodName = method.getName();
CacheOperation attr = this.nameMap.get(methodName); Collection<CacheOperation> attr = this.nameMap.get(methodName);
if (attr == null) { if (attr == null) {
// Look for most specific name match. // Look for most specific name match.

View File

@ -176,6 +176,31 @@ public abstract class AbstractAnnotationTests {
assertSame(r1, service.cache(null)); 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 @Test
public void testCacheable() throws Exception { public void testCacheable() throws Exception {
testCacheable(cs); testCacheable(cs);
@ -284,4 +309,24 @@ public abstract class AbstractAnnotationTests {
public void testClassUncheckedException() throws Exception { public void testClassUncheckedException() throws Exception {
testUncheckedThrowable(ccs); 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);
}
} }

View File

@ -19,6 +19,7 @@ package org.springframework.cache.config;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CacheUpdate;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
/** /**
@ -61,6 +62,16 @@ public class AnnotatedClassCacheableService implements CacheableService {
return counter.getAndIncrement(); 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) { public Object nullValue(Object arg1) {
nullInvocations.incrementAndGet(); nullInvocations.incrementAndGet();
return null; return null;

View File

@ -38,6 +38,10 @@ public interface CacheableService<T> {
T nullValue(Object arg1); T nullValue(Object arg1);
T update(Object arg1);
T conditionalUpdate(Object arg2);
Number nullInvocations(); Number nullInvocations();
T rootVars(Object arg1); T rootVars(Object arg1);

View File

@ -19,6 +19,7 @@ package org.springframework.cache.config;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CacheUpdate;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
/** /**
@ -64,6 +65,16 @@ public class DefaultCacheableService implements CacheableService<Long> {
return counter.getAndIncrement(); 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") @Cacheable("default")
public Long nullValue(Object arg1) { public Long nullValue(Object arg1) {
nullInvocations.incrementAndGet(); nullInvocations.incrementAndGet();