Provide common cache config

Prior to this commit, common cache operation settings had to be
repeated for every operation: cache name(s), custom cache manager
and custom key manager.

This commit introduces the @CacheConfig annotation to bet set at
class-level (either directly or as a meta-annotation). As the cache
name(s) can be rationalized there, the "value" of the various
annotations are no longer mandatory.

CacheAnnotationParser has an API breakage to be able to retrieve
information at class-level.

Issue: SPR-11316
This commit is contained in:
Stephane Nicoll
2014-01-29 01:45:10 +01:00
parent 6a8cf28665
commit 3c28301ded
9 changed files with 357 additions and 62 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 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.
@@ -17,7 +17,6 @@
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;
@@ -40,6 +39,7 @@ import org.springframework.util.Assert;
*
* @author Costin Leau
* @author Juergen Hoeller
* @author Stephane Nicoll
* @since 3.1
*/
@SuppressWarnings("serial")
@@ -106,29 +106,40 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
@Override
protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
return determineCacheOperations(clazz);
protected Collection<CacheOperation> findCacheOperations(final Class<?> clazz) {
return determineCacheOperations(new CacheOperationProvider() {
@Override
public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
return parser.parseCacheAnnotations(clazz);
}
});
}
@Override
protected Collection<CacheOperation> findCacheOperations(Method method) {
return determineCacheOperations(method);
protected Collection<CacheOperation> findCacheOperations(final Method method) {
return determineCacheOperations(new CacheOperationProvider() {
@Override
public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
return parser.parseCacheAnnotations(method);
}
});
}
/**
* Determine the cache operation(s) for the given method or class.
* Determine the cache operation(s) for the given {@link CacheOperationProvider}.
* <p>This implementation delegates to configured
* {@link CacheAnnotationParser}s for parsing known annotations into
* Spring's metadata attribute class.
* <p>Can be overridden to support custom annotations that carry
* caching metadata.
* @param ae the annotated method or class
* @param provider the cache operation provider to use
* @return the configured caching operations, or {@code null} if none found
*/
protected Collection<CacheOperation> determineCacheOperations(AnnotatedElement ae) {
protected Collection<CacheOperation> determineCacheOperations(CacheOperationProvider provider) {
Collection<CacheOperation> ops = null;
for (CacheAnnotationParser annotationParser : this.annotationParsers) {
Collection<CacheOperation> annOps = annotationParser.parseCacheAnnotations(ae);
Collection<CacheOperation> annOps = provider.getCacheOperations(annotationParser);
if (annOps != null) {
if (ops == null) {
ops = new ArrayList<CacheOperation>();
@@ -166,4 +177,19 @@ public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperati
return this.annotationParsers.hashCode();
}
/**
* Callback interface providing {@link CacheOperation} instance(s) based on
* a given {@link CacheAnnotationParser}.
*/
protected interface CacheOperationProvider {
/**
* Returns the {@link CacheOperation} instance(s) provided by the specified parser.
*
* @param parser the parser to use
* @return the cache operations or {@code null} if none is found
*/
Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 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.
@@ -16,7 +16,7 @@
package org.springframework.cache.annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Collection;
import org.springframework.cache.interceptor.CacheOperation;
@@ -28,20 +28,34 @@ import org.springframework.cache.interceptor.CacheOperation;
* {@link Cacheable}, {@link CachePut} or {@link CacheEvict}.
*
* @author Costin Leau
* @author Stephane Nicoll
* @since 3.1
*/
public interface CacheAnnotationParser {
/**
* Parses the cache definition for the given method or class,
* Parses the cache definition for the given class,
* based on a known annotation type.
* <p>This essentially parses a known cache annotation into Spring's
* metadata attribute class. Returns {@code null} if the method/class
* metadata attribute class. Returns {@code null} if the class
* is not cacheable.
* @param ae the annotated method or class
* @param type the annotated class
* @return CacheOperation the configured caching operation,
* or {@code null} if none was found
* @see AnnotationCacheOperationSource#determineCacheOperations(AnnotatedElement)
* @see AnnotationCacheOperationSource#findCacheOperations(Class)
*/
Collection<CacheOperation> parseCacheAnnotations(AnnotatedElement ae);
Collection<CacheOperation> parseCacheAnnotations(Class<?> type);
/**
* Parses the cache definition for the given method,
* based on a known annotation type.
* <p>This essentially parses a known cache annotation into Spring's
* metadata attribute class. Returns {@code null} if the method
* is not cacheable.
* @param method the annotated method
* @return CacheOperation the configured caching operation,
* or {@code null} if none was found
* @see AnnotationCacheOperationSource#findCacheOperations(Method)
*/
Collection<CacheOperation> parseCacheAnnotations(Method method);
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2002-2014 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.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Provide a way to share common cache-related settings at class-level.
* <p>When this annotation is present on a given class, it provides a set
* of default settings for any cache operation defined on that class.
*
* @author Stephane Nicoll
* @since 4.1
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
/**
* Name of the default caches to consider for a caching operation defined in the class.
* <p>If none is set at the operation level, these ones are used instead of the default.
* <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[] cacheNames() default {};
/**
* The bean name of the default {@link org.springframework.cache.interceptor.KeyGenerator} to
* use for the class.
* <p>If none is set at the operation level, this one is used instead of the default.
* <p>The key generator is mutually exclusive with the use of a custom key. When such key is
* defined for the operation, the value of this key generator is ignored.
*/
String keyGenerator() default "";
/**
* The bean name of the custom {@link org.springframework.cache.CacheManager} to use.
* <p>If none is set at the operation level, this one is used instead of the default.
*/
String cacheManager() default "";
}

View File

@@ -30,6 +30,7 @@ import java.lang.annotation.Target;
* @author Costin Leau
* @author Stephane Nicoll
* @since 3.1
* @see CacheConfig
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@@ -42,7 +43,7 @@ public @interface CacheEvict {
* <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();
String[] value() default {};
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.

View File

@@ -35,6 +35,7 @@ import org.springframework.cache.Cache;
* @author Phillip Webb
* @author Stephane Nicoll
* @since 3.1
* @see CacheConfig
*/
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@@ -47,7 +48,7 @@ public @interface CachePut {
* <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();
String[] value() default {};
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.

View File

@@ -33,6 +33,7 @@ import java.lang.annotation.Target;
* @author Phillip Webb
* @author Stephane Nicoll
* @since 3.1
* @see CacheConfig
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@@ -45,7 +46,7 @@ public @interface Cacheable {
* <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();
String[] value() default {};
/**
* Spring Expression Language (SpEL) attribute for computing the key dynamically.

View File

@@ -19,6 +19,7 @@ package org.springframework.cache.annotation;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
@@ -26,6 +27,7 @@ import org.springframework.cache.interceptor.CacheEvictOperation;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CachePutOperation;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -44,35 +46,47 @@ import org.springframework.util.StringUtils;
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
@Override
public Collection<CacheOperation> parseCacheAnnotations(AnnotatedElement ae) {
public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
DefaultCacheConfig defaultConfig = getDefaultCacheConfig(type);
return parseCacheAnnotations(defaultConfig, type);
}
@Override
public Collection<CacheOperation> parseCacheAnnotations(Method method) {
DefaultCacheConfig defaultConfig = getDefaultCacheConfig(method.getDeclaringClass());
return parseCacheAnnotations(defaultConfig, method);
}
protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig,
AnnotatedElement ae) {
Collection<CacheOperation> ops = null;
Collection<Cacheable> cacheables = getAnnotations(ae, Cacheable.class);
if (cacheables != null) {
ops = lazyInit(ops);
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, cacheable));
ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
}
}
Collection<CacheEvict> evicts = getAnnotations(ae, CacheEvict.class);
if (evicts != null) {
ops = lazyInit(ops);
for (CacheEvict e : evicts) {
ops.add(parseEvictAnnotation(ae, e));
ops.add(parseEvictAnnotation(ae, cachingConfig, e));
}
}
Collection<CachePut> updates = getAnnotations(ae, CachePut.class);
if (updates != null) {
ops = lazyInit(ops);
for (CachePut p : updates) {
ops.add(parseUpdateAnnotation(ae, p));
ops.add(parseUpdateAnnotation(ae, cachingConfig, p));
}
}
Collection<Caching> caching = getAnnotations(ae, Caching.class);
if (caching != null) {
ops = lazyInit(ops);
for (Caching c : caching) {
ops.addAll(parseCachingAnnotation(ae, c));
ops.addAll(parseCachingAnnotation(ae, cachingConfig, c));
}
}
return ops;
@@ -82,7 +96,8 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return (ops != null ? ops : new ArrayList<CacheOperation>(1));
}
CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, Cacheable caching) {
CacheableOperation parseCacheableAnnotation(AnnotatedElement ae,
DefaultCacheConfig defaultConfig, Cacheable caching) {
CacheableOperation cuo = new CacheableOperation();
cuo.setCacheNames(caching.value());
cuo.setCondition(caching.condition());
@@ -92,11 +107,14 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
cuo.setCacheManager(caching.cacheManager());
cuo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
defaultConfig.applyDefault(cuo);
validateCacheOperation(ae, cuo);
return cuo;
}
CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, CacheEvict caching) {
CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae,
DefaultCacheConfig defaultConfig, CacheEvict caching) {
CacheEvictOperation ceo = new CacheEvictOperation();
ceo.setCacheNames(caching.value());
ceo.setCondition(caching.condition());
@@ -107,11 +125,14 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
ceo.setBeforeInvocation(caching.beforeInvocation());
ceo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
defaultConfig.applyDefault(ceo);
validateCacheOperation(ae, ceo);
return ceo;
}
CacheOperation parseUpdateAnnotation(AnnotatedElement ae, CachePut caching) {
CacheOperation parseUpdateAnnotation(AnnotatedElement ae,
DefaultCacheConfig defaultConfig, CachePut caching) {
CachePutOperation cuo = new CachePutOperation();
cuo.setCacheNames(caching.value());
cuo.setCondition(caching.condition());
@@ -121,38 +142,56 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
cuo.setCacheManager(caching.cacheManager());
cuo.setName(ae.toString());
checkKeySourceConsistency(ae, caching.key(), caching.keyGenerator());
defaultConfig.applyDefault(cuo);
validateCacheOperation(ae, cuo);
return cuo;
}
Collection<CacheOperation> parseCachingAnnotation(AnnotatedElement ae, Caching caching) {
Collection<CacheOperation> parseCachingAnnotation(AnnotatedElement ae,
DefaultCacheConfig defaultConfig, Caching caching) {
Collection<CacheOperation> ops = null;
Cacheable[] cacheables = caching.cacheable();
if (!ObjectUtils.isEmpty(cacheables)) {
ops = lazyInit(ops);
for (Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, cacheable));
ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable));
}
}
CacheEvict[] evicts = caching.evict();
if (!ObjectUtils.isEmpty(evicts)) {
ops = lazyInit(ops);
for (CacheEvict evict : evicts) {
ops.add(parseEvictAnnotation(ae, evict));
ops.add(parseEvictAnnotation(ae, defaultConfig, evict));
}
}
CachePut[] updates = caching.put();
if (!ObjectUtils.isEmpty(updates)) {
ops = lazyInit(ops);
for (CachePut update : updates) {
ops.add(parseUpdateAnnotation(ae, update));
ops.add(parseUpdateAnnotation(ae, defaultConfig, update));
}
}
return ops;
}
/**
* Provides the {@link DefaultCacheConfig} instance for the specified {@link Class}.
*
* @param target the class-level to handle
* @return the default config (never {@code null})
*/
DefaultCacheConfig getDefaultCacheConfig(Class<?> target) {
final CacheConfig annotation = AnnotationUtils.getAnnotation(target, CacheConfig.class);
if (annotation != null) {
return new DefaultCacheConfig(annotation.cacheManager(),
annotation.keyGenerator(), annotation.cacheNames());
}
return new DefaultCacheConfig();
}
private <T extends Annotation> Collection<T> getAnnotations(AnnotatedElement ae, Class<T> annotationType) {
Collection<T> anns = new ArrayList<T>(2);
@@ -173,13 +212,27 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return (anns.isEmpty() ? null : anns);
}
private void checkKeySourceConsistency(AnnotatedElement ae, String key, String keyGenerator) {
if (StringUtils.hasText(key) && StringUtils.hasText(keyGenerator)) {
/**
* Validates the specified {@link CacheOperation}.
* <p>Throws an {@link IllegalStateException} if the state of the operation is
* invalid. As there might be multiple sources for default values, this ensure
* that the operation is in a proper state before being returned.
*
* @param ae the annotated element of the cache operation
* @param operation the {@link CacheOperation} to validate
*/
private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) {
if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache annotation configuration on '"
+ ae.toString() + "'. Both 'key' and 'keyGenerator' attributes have been set. " +
"These attributes are mutually exclusive: either set the SpEL expression used to" +
"compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
if (operation.getCacheNames().isEmpty()) {
throw new IllegalStateException("No cache names could be detected on '"
+ ae.toString()+ "'. Make sure to set the value parameter on the annotation or" +
"declare a @CacheConfig at the class-level with the default cache name(s) to use.");
}
}
@Override
@@ -192,4 +245,41 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
return SpringCacheAnnotationParser.class.hashCode();
}
/**
* Provides default settings for a given set of cache operations.
*/
static class DefaultCacheConfig {
private final String cacheManager;
private final String keyGenerator;
private final String[] cacheNames;
private DefaultCacheConfig(String cacheManager, String keyGenerator, String[] cacheNames) {
this.cacheManager = cacheManager;
this.keyGenerator = keyGenerator;
this.cacheNames = cacheNames;
}
public DefaultCacheConfig() {
this(null, null, null);
}
/**
* Apply the defaults to the specified {@link CacheOperation}.
*
* @param operation the operation to update
*/
public void applyDefault(CacheOperation operation) {
if (!StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(cacheManager)) {
operation.setCacheManager(cacheManager);
}
if (!StringUtils.hasText(operation.getKey()) && !StringUtils.hasText(operation.getKeyGenerator())
&& StringUtils.hasText(keyGenerator)) {
operation.setKeyGenerator(keyGenerator);
}
if (operation.getCacheNames().isEmpty() && cacheNames != null) {
operation.setCacheNames(cacheNames);
}
}
}
}

View File

@@ -73,10 +73,10 @@ public abstract class CacheOperation {
}
public void setCacheNames(String[] cacheNames) {
Assert.notEmpty(cacheNames);
this.cacheNames = new LinkedHashSet<String>(cacheNames.length);
for (String string : cacheNames) {
this.cacheNames.add(string);
for (String cacheName : cacheNames) {
Assert.hasText(cacheName, "Cache name must be set if specified.");
this.cacheNames.add(cacheName);
}
}