+ add XML support for cache abstraction (cache-advice) - DRAFT

This commit is contained in:
Costin Leau
2011-08-16 17:35:01 +00:00
parent a8476d05be
commit 63a217a40a
15 changed files with 676 additions and 26 deletions

View File

@@ -48,11 +48,6 @@ import static org.springframework.context.annotation.AnnotationConfigUtils.*;
*/
class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser {
private static final String CACHE_MANAGER_ATTRIBUTE = "cache-manager";
private static final String DEFAULT_CACHE_MANAGER_BEAN_NAME = "cacheManager";
/**
* Parses the '<code>&lt;cache:annotation-driven/&gt;</code>' tag. Will
* {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary register an AutoProxyCreator}
@@ -71,13 +66,9 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser
return null;
}
private static String extractCacheManager(Element element) {
return (element.hasAttribute(CACHE_MANAGER_ATTRIBUTE) ? element.getAttribute(CACHE_MANAGER_ATTRIBUTE)
: DEFAULT_CACHE_MANAGER_BEAN_NAME);
}
private static void registerCacheManagerProperty(Element element, BeanDefinition def) {
def.getPropertyValues().add("cacheManager", new RuntimeBeanReference(extractCacheManager(element)));
def.getPropertyValues().add("cacheManager",
new RuntimeBeanReference(CacheNamespaceHandler.extractCacheManager(element)));
}
/**
@@ -124,7 +115,7 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser
interceptorDef.setSource(eleSource);
interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registerCacheManagerProperty(element, interceptorDef);
interceptorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName));
interceptorDef.getPropertyValues().add("cacheOperationSources", new RuntimeBeanReference(sourceName));
String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef);
// Create the CacheAdvisor definition.

View File

@@ -0,0 +1,171 @@
/*
* 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.config;
import java.util.List;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.parsing.ReaderContext;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
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.NameMatchCacheOperationSource;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
/**
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser
* BeanDefinitionParser} for the {@code <tx:advice/>} tag.
*
* @author Costin Leau
*
*/
class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
/**
* Simple, reusable class used for overriding defaults.
*
* @author Costin Leau
*/
private static class Props {
private String key, condition;
private String[] caches = null;
Props(Element root) {
String defaultCache = root.getAttribute("cache");
key = root.getAttribute("key");
condition = root.getAttribute("condition");
if (StringUtils.hasText(defaultCache)) {
caches = StringUtils.commaDelimitedListToStringArray(defaultCache.trim());
}
}
CacheOperation merge(Element element, ReaderContext readerCtx, CacheOperation op) {
String cache = element.getAttribute("cache");
String k = element.getAttribute("key");
String c = element.getAttribute("condition");
String[] localCaches = caches;
String localKey = key, localCondition = condition;
// sanity check
if (StringUtils.hasText(cache)) {
localCaches = StringUtils.commaDelimitedListToStringArray(cache.trim());
} else {
if (caches == null) {
readerCtx.error("No cache specified specified for " + element.getNodeName(), element);
}
}
if (StringUtils.hasText(k)) {
localKey = k.trim();
}
if (StringUtils.hasText(c)) {
localCondition = c.trim();
}
op.setCacheNames(localCaches);
op.setKey(localKey);
op.setCondition(localCondition);
return op;
}
}
private static final String CACHEABLE_ELEMENT = "cacheable";
private static final String CACHE_EVICT_ELEMENT = "cache-evict";
private static final String METHOD_ATTRIBUTE = "method";
private static final String DEFS_ELEMENT = "definitions";
@Override
protected Class<?> getBeanClass(Element element) {
return CacheInterceptor.class;
}
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
builder.addPropertyReference("cacheManager", CacheNamespaceHandler.extractCacheManager(element));
List<Element> cacheDefs = DomUtils.getChildElementsByTagName(element, DEFS_ELEMENT);
if (cacheDefs.size() >= 1) {
// Using attributes source.
List<RootBeanDefinition> attributeSourceDefinitions = parseDefinitionsSources(cacheDefs, parserContext);
builder.addPropertyValue("cacheOperationSources", attributeSourceDefinitions);
} else {
// Assume annotations source.
builder.addPropertyValue("cacheOperationSources", new RootBeanDefinition(
AnnotationCacheOperationSource.class));
}
}
private List<RootBeanDefinition> parseDefinitionsSources(List<Element> definitions, ParserContext parserContext) {
ManagedList<RootBeanDefinition> defs = new ManagedList<RootBeanDefinition>(definitions.size());
// extract default param for the definition
for (Element element : definitions) {
defs.add(parseDefinitionSource(element, parserContext));
}
return defs;
}
private RootBeanDefinition parseDefinitionSource(Element definition, ParserContext parserContext) {
Props prop = new Props(definition);
// add cacheable first
ManagedMap<TypedStringValue, CacheOperation> cacheOpeMap = new ManagedMap<TypedStringValue, CacheOperation>();
cacheOpeMap.setSource(parserContext.extractSource(definition));
List<Element> updateCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHEABLE_ELEMENT);
for (Element opElement : updateCacheMethods) {
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());
cacheOpeMap.put(nameHolder, op);
}
List<Element> evictCacheMethods = DomUtils.getChildElementsByTagName(definition, CACHE_EVICT_ELEMENT);
for (Element opElement : evictCacheMethods) {
String name = opElement.getAttribute(METHOD_ATTRIBUTE);
TypedStringValue nameHolder = new TypedStringValue(name);
nameHolder.setSource(parserContext.extractSource(opElement));
CacheOperation op = prop.merge(opElement, parserContext.getReaderContext(), new CacheEvictOperation());
cacheOpeMap.put(nameHolder, op);
}
RootBeanDefinition attributeSourceDefinition = new RootBeanDefinition(NameMatchCacheOperationSource.class);
attributeSourceDefinition.setSource(parserContext.extractSource(definition));
attributeSourceDefinition.getPropertyValues().add("nameMap", cacheOpeMap);
return attributeSourceDefinition;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cache.config;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.w3c.dom.Element;
/**
* <code>NamespaceHandler</code> allowing for the configuration of
@@ -30,8 +31,18 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
*/
public class CacheNamespaceHandler extends NamespaceHandlerSupport {
static final String CACHE_MANAGER_ATTRIBUTE = "cache-manager";
static final String DEFAULT_CACHE_MANAGER_BEAN_NAME = "cacheManager";
static String extractCacheManager(Element element) {
return (element.hasAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE) ? element
.getAttribute(CacheNamespaceHandler.CACHE_MANAGER_ATTRIBUTE)
: CacheNamespaceHandler.DEFAULT_CACHE_MANAGER_BEAN_NAME);
}
public void init() {
registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenCacheBeanDefinitionParser());
registerBeanDefinitionParser("advice", new CacheAdviceParser());
}
}

View File

@@ -117,11 +117,6 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
return new DefaultCacheKey(method, targetClass);
}
/**
* Same signature as {@link #getTransactionAttribute}, but doesn't cache the result.
* {@link #getTransactionAttribute} is effectively a caching decorator for this method.
* @see #getTransactionAttribute
*/
private CacheOperation computeCacheOperationDefinition(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {

View File

@@ -44,12 +44,8 @@ import org.springframework.util.StringUtils;
*
* <p>Subclasses are responsible for calling methods in this class in the correct order.
*
* <p>If no caching name has been specified in the <code>CacheOperationDefinition</code>,
* the exposed name will be the <code>fully-qualified class name + "." + method name</code>
* (by default).
*
* <p>Uses the <b>Strategy</b> design pattern. A <code>CacheManager</code>
* implementation will perform the actual transaction management, and a
* implementation will perform the actual cache management, and a
* <code>CacheDefinitionSource</code> is used for determining caching operation definitions.
*
* <p>A cache aspect is serializable if its <code>CacheManager</code>
@@ -155,6 +151,7 @@ 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());

View File

@@ -0,0 +1,84 @@
/*
* 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 CacheUpdateOperation();
}
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

@@ -0,0 +1,56 @@
/*
* Copyright 2010-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 org.springframework.aop.Pointcut;
import org.springframework.aop.framework.AbstractSingletonProxyFactoryBean;
/**
* Proxy factory bean for simplified declarative caching handling.
* This is a convenient alternative to a standard AOP
* {@link org.springframework.aop.framework.ProxyFactoryBean}
* with a separate {@link CachingInterceptor} definition.
*
* <p>This class is intended to cover the <i>typical</i> case of declarative
* cache demarcation: namely, wrapping a singleton target object with a
* caching proxy, proxying all the interfaces that the target implements.
*
* @author Costin Leau
* @see org.springframework.aop.framework.ProxyFactoryBean
* @see CachingInterceptor
*/
public class CacheProxyFactoryBean extends AbstractSingletonProxyFactoryBean {
private final CacheInterceptor cachingInterceptor = new CacheInterceptor();
private Pointcut pointcut;
@Override
protected Object createMainInterceptor() {
return null;
}
/**
* Set the caching attribute source which is used to find the cache operation
* definition.
*
* @param cacheDefinitionSources cache definition sources
*/
public void setCacheDefinitionSources(CacheOperationSource... cacheDefinitionSources) {
this.cachingInterceptor.setCacheOperationSources(cacheDefinitionSources);
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.io.Serializable;
import java.lang.reflect.Method;
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;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PatternMatchUtils;
/**
* Simple {@link CacheOperationSource} implementation that
* allows attributes to be matched by registered name.
*
* @author Costin Leau
*/
public class NameMatchCacheOperationSource implements CacheOperationSource, Serializable {
/**
* Logger available to subclasses.
* <p>Static for optimal serialization.
*/
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>();
/**
* Set a name/attribute map, consisting of method names
* (e.g. "myMethod") and CacheOperation instances
* (or Strings to be converted to CacheOperation instances).
* @see CacheOperation
* @see CacheOperationEditor
*/
public void setNameMap(Map<String, CacheOperation> nameMap) {
for (Map.Entry<String, 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
*/
public void addCacheMethod(String methodName, CacheOperation operation) {
if (logger.isDebugEnabled()) {
logger.debug("Adding method [" + methodName + "] with cache operation [" + operation + "]");
}
this.nameMap.put(methodName, operation);
}
public CacheOperation getCacheOperation(Method method, Class<?> targetClass) {
// look for direct name match
String methodName = method.getName();
CacheOperation attr = this.nameMap.get(methodName);
if (attr == 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);
bestNameMatch = mappedName;
}
}
}
return attr;
}
/**
* Return if the given method name matches the mapped name.
* <p>The default implementation checks for "xxx*", "*xxx" and "*xxx*" matches,
* as well as direct equality. Can be overridden in subclasses.
* @param methodName the method name of the class
* @param mappedName the name in the descriptor
* @return if the names match
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
*/
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof NameMatchCacheOperationSource)) {
return false;
}
NameMatchCacheOperationSource otherTas = (NameMatchCacheOperationSource) other;
return ObjectUtils.nullSafeEquals(this.nameMap, otherTas.nameMap);
}
@Override
public int hashCode() {
return NameMatchCacheOperationSource.class.hashCode();
}
@Override
public String toString() {
return getClass().getName() + ": " + this.nameMap;
}
}