SPR-8082
SPR-7833
+ add support for CacheDefinitions declarations inside XML
+ more integration tests
This commit is contained in:
Costin Leau
2011-11-09 17:53:51 +00:00
parent e4c88553d8
commit dc88a7c8ba
20 changed files with 497 additions and 175 deletions

View File

@@ -34,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}, {@link CacheUpdate} and {@link CacheEvict}
* <p>This class reads Spring's JDK 1.5+ {@link Cacheable}, {@link CachePut} 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.
*

View File

@@ -26,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}, {@link CacheUpdate} or {@link CacheEvict}.
* {@link Cacheable}, {@link CachePut} or {@link CacheEvict}.
*
* @author Costin Leau
* @since 3.1

View File

@@ -24,7 +24,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Group annotation for multiple cacheable annotations (of different or the same type).
* Group annotation for multiple cache annotations (of different or the same type).
*
* @author Costin Leau
* @since 3.1
@@ -33,11 +33,11 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheDefinition {
public @interface CacheDefinitions {
Cacheable[] cacheables();
Cacheable[] cacheable() default {};
CacheUpdate[] updates();
CachePut[] put() default {};
CacheEvict[] evicts();
CacheEvict[] evict() default {};
}

View File

@@ -23,11 +23,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cache.Cache;
/**
*
* 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
* a {@link Cache#put(Object, Object)} operation. As opposed to {@link Cacheable} annotation,
* this annotation does not cause the target method to be skipped - rather it
* always causes the method to be invoked and its result to be placed into the cache.
*
* @author Costin Leau
@@ -37,7 +39,7 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheUpdate {
public @interface CachePut {
/**
* Name of the caches in which the update takes place.

View File

@@ -23,13 +23,13 @@ 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.CachePutOperation;
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}, {@link CacheEvict} and {@link CacheUpdate} annotations.
* Strategy implementation for parsing Spring's {@link Cacheable}, {@link CacheEvict} and {@link CachePut} annotations.
*
* @author Costin Leau
* @author Juergen Hoeller
@@ -51,12 +51,12 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
ops = lazyInit(ops);
ops.add(parseEvictAnnotation(ae, evict));
}
CacheUpdate update = AnnotationUtils.getAnnotation(ae, CacheUpdate.class);
CachePut update = AnnotationUtils.getAnnotation(ae, CachePut.class);
if (update != null) {
ops = lazyInit(ops);
ops.add(parseUpdateAnnotation(ae, update));
}
CacheDefinition definition = AnnotationUtils.getAnnotation(ae, CacheDefinition.class);
CacheDefinitions definition = AnnotationUtils.getAnnotation(ae, CacheDefinitions.class);
if (definition != null) {
ops = lazyInit(ops);
ops.addAll(parseDefinitionAnnotation(ae, definition));
@@ -87,8 +87,8 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return ceo;
}
CacheOperation parseUpdateAnnotation(AnnotatedElement ae, CacheUpdate ann) {
CacheUpdateOperation cuo = new CacheUpdateOperation();
CacheOperation parseUpdateAnnotation(AnnotatedElement ae, CachePut ann) {
CachePutOperation cuo = new CachePutOperation();
cuo.setCacheNames(ann.value());
cuo.setCondition(ann.condition());
cuo.setKey(ann.key());
@@ -96,27 +96,27 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return cuo;
}
Collection<CacheOperation> parseDefinitionAnnotation(AnnotatedElement ae, CacheDefinition ann) {
Collection<CacheOperation> parseDefinitionAnnotation(AnnotatedElement ae, CacheDefinitions ann) {
Collection<CacheOperation> ops = null;
Cacheable[] cacheables = ann.cacheables();
Cacheable[] cacheables = ann.cacheable();
if (!ObjectUtils.isEmpty(cacheables)) {
ops = lazyInit(ops);
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, cacheable));
}
}
CacheEvict[] evicts = ann.evicts();
CacheEvict[] evicts = ann.evict();
if (!ObjectUtils.isEmpty(evicts)) {
ops = lazyInit(ops);
for (CacheEvict evict : evicts) {
ops.add(parseEvictAnnotation(ae, evict));
}
}
CacheUpdate[] updates = ann.updates();
CachePut[] updates = ann.put();
if (!ObjectUtils.isEmpty(updates)) {
ops = lazyInit(ops);
for (CacheUpdate update : updates) {
for (CachePut update : updates) {
ops.add(parseUpdateAnnotation(ae, update));
}
}

View File

@@ -16,6 +16,8 @@
package org.springframework.cache.config;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.config.TypedStringValue;
@@ -30,6 +32,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.CachePutOperation;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.cache.interceptor.NameMatchCacheOperationSource;
import org.springframework.util.StringUtils;
@@ -52,13 +55,14 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
*/
private static class Props {
private String key, condition;
private String key, condition, method;
private String[] caches = null;
Props(Element root) {
String defaultCache = root.getAttribute("cache");
key = root.getAttribute("key");
condition = root.getAttribute("condition");
method = root.getAttribute(METHOD_ATTRIBUTE);
if (StringUtils.hasText(defaultCache)) {
caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim());
@@ -95,10 +99,24 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
return op;
}
String merge(Element element, ReaderContext readerCtx) {
String m = element.getAttribute(METHOD_ATTRIBUTE);
if (StringUtils.hasText(m)) {
return m.trim();
}
if (StringUtils.hasText(method)) {
return method;
}
readerCtx.error("No method specified for " + element.getNodeName(), element);
return null;
}
}
private static final String CACHEABLE_ELEMENT = "cacheable";
private static final String CACHE_EVICT_ELEMENT = "cache-evict";
private static final String CACHE_PUT_ELEMENT = "cache-put";
private static final String METHOD_ATTRIBUTE = "method";
private static final String DEFS_ELEMENT = "definitions";
@@ -139,34 +157,60 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
Props prop = new Props(definition);
// add cacheable first
ManagedMap<TypedStringValue, CacheOperation> cacheOpeMap = new ManagedMap<TypedStringValue, CacheOperation>();
cacheOpeMap.setSource(parserContext.extractSource(definition));
ManagedMap<TypedStringValue, Collection<CacheOperation>> cacheOpMap = new ManagedMap<TypedStringValue, Collection<CacheOperation>>();
cacheOpMap.setSource(parserContext.extractSource(definition));
List<Element> updateCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT);
List<Element> cacheableCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT);
for (Element opElement : updateCacheMethods) {
String name = opElement.getAttribute(METHOD_ATTRIBUTE);
for (Element opElement : cacheableCacheMethods) {
String name = prop.merge(opElement, parserContext.getReaderContext());
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheableOperation());
cacheOpeMap.put(nameHolder, op);
Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
if (col == null) {
col = new ArrayList<CacheOperation>(2);
cacheOpMap.put(nameHolder, col);
}
col.add(op);
}
List<Element> evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT);
for (Element opElement : evictCacheMethods) {
String name = opElement.getAttribute(METHOD_ATTRIBUTE);
String name = prop.merge(opElement, parserContext.getReaderContext());
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheEvictOperation());
cacheOpeMap.put(nameHolder, op);
Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
if (col == null) {
col = new ArrayList<CacheOperation>(2);
cacheOpMap.put(nameHolder, col);
}
col.add(op);
}
List<Element> putCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_PUT_ELEMENT);
for (Element opElement : putCacheMethods) {
String name = prop.merge(opElement, parserContext.getReaderContext());
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CachePutOperation());
Collection<CacheOperation> col = cacheOpMap.get(nameHolder);
if (col == null) {
col = new ArrayList<CacheOperation>(2);
cacheOpMap.put(nameHolder, col);
}
col.add(op);
}
RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchCacheOperationSource.class);
attributeSourceDefinition.setSource(parserContext.extractSource(definition));
attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpeMap);
attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpMap);
return attributeSourceDefinition;
}
}

View File

@@ -403,7 +403,7 @@ public abstract class CacheAspectSupport implements InitializingBean {
evicts.add(opContext);
}
if (cacheOperation instanceof CacheUpdateOperation) {
if (cacheOperation instanceof CachePutOperation) {
updates.add(opContext);
}
}

View File

@@ -1,84 +0,0 @@
/*
* 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.interceptor;
import java.beans.PropertyEditorSupport;
import org.springframework.util.StringUtils;
/**
* PropertyEditor for {@link CacheOperation} objects. Accepts a String of form
* <p><tt>action,cache,key,condition</tt>
* <p>where only action and cache are required. Available definitions for action are
* <tt>cacheable</tt> and <tt>evict</tt>.
* When specifying multiple caches, use ; as a separator
*
* A typical example would be:
* <p><code>cacheable, orders;books, #p0</code>
*
* <p>The tokens need to be specified in the order above.
*
* @author Costin Leau
*
* @see org.springframework.transaction.TransactionAttributeEditor
* @see org.springframework.core.Constants
*/
public class CacheOperationEditor extends PropertyEditorSupport {
/**
* Format is action, cache, key, condition.
* Null or the empty string means that the method is non cacheable.
* @see java.beans.PropertyEditor#setAsText(java.lang.String)
*/
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (StringUtils.hasLength(text)) {
// tokenize it with ","
String[] tokens = StringUtils.commaDelimitedListToStringArray(text);
if (tokens.length < 2) {
throw new IllegalArgumentException(
"too little arguments found, at least the cache action and cache name are required");
}
CacheOperation op;
if ("cacheable".contains(tokens[0])) {
op = new CacheableOperation();
}
else if ("evict".contains(tokens[0])) {
op = new CacheEvictOperation();
} else {
throw new IllegalArgumentException("Invalid cache action specified " + tokens[0]);
}
op.setCacheNames(StringUtils.delimitedListToStringArray(tokens[1], ";"));
if (tokens.length > 2) {
op.setKey(tokens[2]);
}
if (tokens.length > 3) {
op.setCondition(tokens[3]);
}
setValue(op);
} else {
setValue(null);
}
}
}

View File

@@ -17,11 +17,11 @@
package org.springframework.cache.interceptor;
/**
* Class describing a cache 'update' operation.
* Class describing a cache 'put' operation.
*
* @author Costin Leau
* @since 3.1
*/
public class CacheUpdateOperation extends CacheOperation {
public class CachePutOperation extends CacheOperation {
}

View File

@@ -19,11 +19,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;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -54,63 +51,44 @@ public class NameMatchCacheOperationSource implements CacheOperationSource, Seri
* @see CacheOperation
* @see CacheOperationEditor
*/
public void setNameMap(Map<String, CacheOperation> nameMap) {
for (Map.Entry<String, CacheOperation> entry : nameMap.entrySet()) {
public void setNameMap(Map<String, Collection<CacheOperation>> nameMap) {
for (Map.Entry<String, Collection<CacheOperation>> entry : nameMap.entrySet()) {
addCacheMethod(entry.getKey(), entry.getValue());
}
}
/**
* Parses the given properties into a name/attribute map.
* Expects method names as keys and String attributes definitions as values,
* parsable into CacheOperation instances via CacheOperationEditor.
* @see #setNameMap
* @see CacheOperationEditor
*/
public void setProperties(Properties cacheOperations) {
CacheOperationEditor tae = new CacheOperationEditor();
Enumeration propNames = cacheOperations.propertyNames();
while (propNames.hasMoreElements()) {
String methodName = (String) propNames.nextElement();
String value = cacheOperations.getProperty(methodName);
tae.setAsText(value);
CacheOperation op = (CacheOperation) tae.getValue();
addCacheMethod(methodName, op);
}
}
/**
* Add an attribute for a cacheable method.
* <p>Method names can be exact matches, or of the pattern "xxx*",
* "*xxx" or "*xxx*" for matching multiple methods.
* @param methodName the name of the method
* @param operation operation associated with the method
* @param ops operation associated with the method
*/
public void addCacheMethod(String methodName, CacheOperation operation) {
public void addCacheMethod(String methodName, Collection<CacheOperation> ops) {
if (logger.isDebugEnabled()) {
logger.debug("Adding method [" + methodName + "] with cache operation [" + operation + "]");
logger.debug("Adding method [" + methodName + "] with cache operations [" + ops + "]");
}
this.nameMap.put(methodName, Collections.singleton(operation));
this.nameMap.put(methodName, ops);
}
public Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass) {
// look for direct name match
String methodName = method.getName();
Collection<CacheOperation> attr = this.nameMap.get(methodName);
Collection<CacheOperation> ops = this.nameMap.get(methodName);
if (attr == null) {
if (ops == null) {
// Look for most specific name match.
String bestNameMatch = null;
for (String mappedName : this.nameMap.keySet()) {
if (isMatch(methodName, mappedName)
&& (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) {
attr = this.nameMap.get(mappedName);
ops = this.nameMap.get(mappedName);
bestNameMatch = mappedName;
}
}
}
return attr;
return ops;
}
/**

View File

@@ -182,35 +182,29 @@
The SpEL expression used for conditioning the method caching.]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="definitionType">
<xsd:complexContent>
<xsd:extension base="basedefinitionType">
<xsd:attribute name="method" type="xsd:string" use="required">
<xsd:annotation>
<xsd:documentation><![CDATA[
<xsd:attribute name="method" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
The method name(s) with which the cache attributes are to be
associated. The wildcard (*) character can be used to associate the
same cache attribute settings with a number of methods; for
example, 'get*', 'handle*', '*Order', 'on*Event', etc.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:extension>
</xsd:complexContent>
example, 'get*', 'handle*', '*Order', 'on*Event', etc.]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="definitionsType">
<xsd:complexContent>
<xsd:extension base="basedefinitionType">
<xsd:sequence>
<xsd:choice>
<xsd:element name="cacheable" minOccurs="0" maxOccurs="unbounded" type="definitionType"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="cacheable" minOccurs="0" maxOccurs="unbounded" type="basedefinitionType"/>
<xsd:element name="cache-put" minOccurs="0" maxOccurs="unbounded" type="basedefinitionType"/>
<xsd:element name="cache-evict" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType>
<xsd:complexContent>
<xsd:extension base="definitionType">
<xsd:extension base="basedefinitionType">
<xsd:attribute name="all-entries" type="xsd:boolean" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[