+ 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.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.
*
* <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.
* 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<CacheOperation> findCacheOperations(Class<?> clazz) {
return determineCacheOperations(clazz);
}
@Override
protected CacheOperation findCacheOperation(Method method) {
return determineCacheOperation(method);
protected Collection<CacheOperation> findCacheOperations(Method method) {
return determineCacheOperations(method);
}
/**
@ -103,14 +105,19 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
* @return CacheOperation the configured caching operation,
* 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) {
CacheOperation attr = annotationParser.parseCacheAnnotation(ae);
if (attr != null) {
return attr;
Collection<CacheOperation> annOps = annotationParser.parseCacheAnnotations(ae);
if (annOps != null) {
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() {
return this.publicMethodsOnly;
}
}

View File

@ -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 <code>null</code> if none was found
* @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.
*/
String condition() default "";
}

View File

@ -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<CacheOperation> parseCacheAnnotations(AnnotatedElement ae) {
Collection<CacheOperation> 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<CacheOperation> lazyInit(Collection<CacheOperation> ops) {
return (ops != null ? ops : new ArrayList<CacheOperation>(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<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.
* Hooked into Spring's caching interception infrastructure
* via CacheDefinitionSource implementation.
* via CacheOperationSource implementation.
*
*/
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.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);
}

View File

@ -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<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();
/**
* 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
* 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
* 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.
Object cacheKey = getCacheKey(method, targetClass);
CacheOperation cached = this.attributeCache.get(cacheKey);
Collection<CacheOperation> 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<CacheOperation> 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<CacheOperation> 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<CacheOperation> 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 <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
@ -170,7 +172,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
* @return all caching attribute associated with this class
* (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?
@ -213,5 +215,4 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
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.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;
* <code>CacheDefinitionSource</code> is used for determining caching operation definitions.
*
* <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 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<Cache> getCaches(CacheOperation operation) {
Set<String> cacheNames = operation.getCacheNames();
Collection<Cache> caches = new ArrayList<Cache>(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<CacheOperation> cacheOp = getCacheOperationSource().getCacheOperations(method, targetClass);
// analyze caching information
if (cacheOp != null) {
CacheOperationContext context = getOperationContext(cacheOp, method, args, target, targetClass);
Collection<Cache> caches = context.getCaches();
if (!CollectionUtils.isEmpty(cacheOp)) {
Map<String, Collection<CacheOperationContext>> 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<Cache> 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<CacheOperationContext, Object> 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);
}
if (status != null) {
if (status.updateRequired) {
updates.putAll(status.cUpdates);
}
// return cached object
else {
if (log) {
logger.trace("Key " + key + " found in cache, returning value " + retVal);
}
return status.retVal;
}
}
if (cacheOp instanceof CacheEvictOperation) {
CacheEvictOperation evictOp = (CacheEvictOperation) cacheOp;
// for each cache
// lazy key initialization
Object key = null;
for (Cache cache : caches) {
// flush the cache (ignore arguments)
if (evictOp.isCacheWide()) {
cache.clear();
if (log) {
logger.trace("Invalidating entire cache for definition " + cacheOp +
" on method " + method);
}
}
else {
// check key
if (key == null) {
key = context.generateKey();
}
if (log) {
logger.trace("Invalidating cache key " + key + " for definition " + cacheOp
+ " on method " + method);
}
cache.evict(key);
}
}
retVal = invoker.invoke();
if (!updates.isEmpty()) {
update(updates, retVal);
}
return retVal;
}
else {
if (log) {
logger.trace("Cache condition failed on method " + method + " for definition " + cacheOp);
}
}
}
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
// lazy key initialization
Object key = null;
for (Cache cache : context.getCaches()) {
// cache-wide flush
if (evictOp.isCacheWide()) {
cache.clear();
if (log) {
logger.trace("Invalidating entire cache for definition " + evictOp + " on method " + context.method);
}
} else {
// check key
if (key == null) {
key = context.generateKey();
}
if (log) {
logger.trace("Invalidating cache key " + key + " for definition " + evictOp + " on method " + context.method);
}
cache.evict(key);
}
}
}
else {
if (log) {
logger.trace("Cache condition failed on method " + context.method + " for definition " + context.operation);
}
}
}
}
}
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 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 {
@ -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;
}
}
}

View File

@ -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])) {

View File

@ -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 <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 targetClass the target class (may be <code>null</code>,
* in which case the declaring class of the method must be used)
* @return {@link CacheOperation} the matching cache operation,
* 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 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

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");
* 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.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<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
Collection<CacheOperation> 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;
}
}
return null;
Collection<CacheOperation> cacheOperations = source.getCacheOperations(method, targetClass);
if (cacheOperations != null) {
if (ops == null) {
ops = new ArrayList<CacheOperation>();
}
ops.addAll(cacheOperations);
}
}
return ops;
}
}

View File

@ -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<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
@ -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<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
// look for direct name match
String methodName = method.getName();
CacheOperation attr = this.nameMap.get(methodName);
Collection<CacheOperation> attr = this.nameMap.get(methodName);
if (attr == null) {
// Look for most specific name match.

View File

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

View File

@ -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;

View File

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

View File

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