15 changed files with 676 additions and 26 deletions
@ -0,0 +1,171 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,84 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -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); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,148 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
/* |
||||
* 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.config; |
||||
|
||||
|
||||
/** |
||||
* @author Costin Leau |
||||
*/ |
||||
public class CacheAdviceNamespaceTests extends AbstractAnnotationTests { |
||||
|
||||
|
||||
@Override |
||||
protected String getConfig() { |
||||
return "/org/springframework/cache/config/cache-advice.xml"; |
||||
} |
||||
} |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns:aop="http://www.springframework.org/schema/aop" |
||||
xmlns:cache="http://www.springframework.org/schema/cache" |
||||
xmlns:p="http://www.springframework.org/schema/p" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd |
||||
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd |
||||
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> |
||||
|
||||
<cache:advice id="cacheAdviceInterface" cache-manager="cacheManager"> |
||||
<cache:definitions cache="default"> |
||||
<cache:cacheable method="cache"/> |
||||
<cache:cacheable method="conditional" condition="#classField == 3"/> |
||||
<cache:cacheable method="key" key="#p0"/> |
||||
<cache:cacheable method="nam*" key="#root.methodName"/> |
||||
<cache:cacheable method="rootVars" key="#root.methodName + #root.method.name + #root.targetClass + #root.target"/> |
||||
<cache:cacheable method="nullValue" cache="default"/> |
||||
</cache:definitions> |
||||
<cache:definitions> |
||||
<cache:cache-evict method="invalidate" cache="default"/> |
||||
<cache:cache-evict method="evict" key="#p0" cache="default"/> |
||||
</cache:definitions> |
||||
</cache:advice> |
||||
|
||||
<cache:advice id="cacheAdviceClass" cache-manager="cacheManager"> |
||||
<cache:definitions cache="default"> |
||||
<cache:cacheable method="key" key="#p0"/> |
||||
<cache:cacheable method="nam*" key="#root.methodName + #root.caches[0].name"/> |
||||
<cache:cacheable method="rootVars" key="#root.methodName + #root.method.name + #root.targetClass + #root.target"/> |
||||
<cache:cacheable method="cache"/> |
||||
<cache:cacheable method="conditional"/> |
||||
<cache:cacheable method="null*"/> |
||||
</cache:definitions> |
||||
<cache:definitions> |
||||
<cache:cache-evict method="invalidate" cache="default"/> |
||||
<cache:cache-evict method="evict" key="#p0" cache="default"/> |
||||
</cache:definitions> |
||||
</cache:advice> |
||||
|
||||
<aop:config> |
||||
<aop:advisor advice-ref="cacheAdviceInterface" pointcut="execution(* *..DefaultCacheableService.*(..))" order="1"/> |
||||
<aop:advisor advice-ref="cacheAdviceClass" pointcut="execution(* *..AnnotatedClassCacheableService.*(..))" order="1"/> |
||||
<aop:advisor advice-ref="debugInterceptor" pointcut="execution(* *..CacheableService.*(..))" order="2"/> |
||||
</aop:config> |
||||
|
||||
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> |
||||
<property name="caches"> |
||||
<set> |
||||
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> |
||||
</set> |
||||
</property> |
||||
</bean> |
||||
|
||||
<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/> |
||||
|
||||
<bean id="service" class="org.springframework.cache.config.DefaultCacheableService"/> |
||||
|
||||
<bean id="classService" class="org.springframework.cache.config.AnnotatedClassCacheableService"/> |
||||
</beans> |
||||
Loading…
Reference in new issue