diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index f910e812b57..438c8e633fe 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -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 findCacheOperations(Class clazz) { - return determineCacheOperations(clazz); + protected Collection findCacheOperations(final Class clazz) { + return determineCacheOperations(new CacheOperationProvider() { + @Override + public Collection getCacheOperations(CacheAnnotationParser parser) { + return parser.parseCacheAnnotations(clazz); + } + }); + } @Override - protected Collection findCacheOperations(Method method) { - return determineCacheOperations(method); + protected Collection findCacheOperations(final Method method) { + return determineCacheOperations(new CacheOperationProvider() { + @Override + public Collection 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}. *

This implementation delegates to configured * {@link CacheAnnotationParser}s for parsing known annotations into * Spring's metadata attribute class. *

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 determineCacheOperations(AnnotatedElement ae) { + protected Collection determineCacheOperations(CacheOperationProvider provider) { Collection ops = null; for (CacheAnnotationParser annotationParser : this.annotationParsers) { - Collection annOps = annotationParser.parseCacheAnnotations(ae); + Collection annOps = provider.getCacheOperations(annotationParser); if (annOps != null) { if (ops == null) { ops = new ArrayList(); @@ -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 getCacheOperations(CacheAnnotationParser parser); + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java index a1d8a79c808..80bafb871a7 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java @@ -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. *

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 parseCacheAnnotations(AnnotatedElement ae); + Collection parseCacheAnnotations(Class type); + + /** + * Parses the cache definition for the given method, + * based on a known annotation type. + *

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 parseCacheAnnotations(Method method); } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java new file mode 100644 index 00000000000..0c30d960253 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -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. + *

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. + *

If none is set at the operation level, these ones are used instead of the default. + *

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. + *

If none is set at the operation level, this one is used instead of the default. + *

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. + *

If none is set at the operation level, this one is used instead of the default. + */ + String cacheManager() default ""; +} + + diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java index b4036a13c00..5a4529b8e77 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java @@ -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 { *

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. diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java index b65983805ab..81f7f463bd2 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java @@ -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 { *

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. diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 303474ab9eb..64eefc14633 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -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 { *

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. diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java index 76ff7ae624d..87e31a1eee0 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -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 parseCacheAnnotations(AnnotatedElement ae) { + public Collection parseCacheAnnotations(Class type) { + DefaultCacheConfig defaultConfig = getDefaultCacheConfig(type); + return parseCacheAnnotations(defaultConfig, type); + } + + @Override + public Collection parseCacheAnnotations(Method method) { + DefaultCacheConfig defaultConfig = getDefaultCacheConfig(method.getDeclaringClass()); + return parseCacheAnnotations(defaultConfig, method); + } + + protected Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, + AnnotatedElement ae) { Collection ops = null; Collection 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 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 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 = 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(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 parseCachingAnnotation(AnnotatedElement ae, Caching caching) { + Collection parseCachingAnnotation(AnnotatedElement ae, + DefaultCacheConfig defaultConfig, Caching caching) { Collection 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 Collection getAnnotations(AnnotatedElement ae, Class annotationType) { Collection anns = new ArrayList(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}. + *

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); + } + } + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index 64e947c2744..2e1787ec87b 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -73,10 +73,10 @@ public abstract class CacheOperation { } public void setCacheNames(String[] cacheNames) { - Assert.notEmpty(cacheNames); this.cacheNames = new LinkedHashSet(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); } } diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java index d7ce49f1863..f8ae3ebb46a 100644 --- a/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java +++ b/spring-context/src/test/java/org/springframework/cache/annotation/AnnotationCacheOperationSourceTests.java @@ -26,7 +26,9 @@ import java.lang.reflect.Method; import java.util.Collection; import java.util.Iterator; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CacheableOperation; @@ -38,24 +40,33 @@ import org.springframework.util.ReflectionUtils; */ public class AnnotationCacheOperationSourceTests { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + private AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); - private Collection getOps(String name) { - Method method = ReflectionUtils.findMethod(AnnotatedClass.class, name); - return source.getCacheOperations(method, AnnotatedClass.class); + private Collection getOps(Class target, String name, + int expectedNumberOfOperations) { + Collection result = getOps(target, name); + assertEquals("Wrong number of operation(s) for '"+name+"'", + expectedNumberOfOperations, result.size()); + return result; + } + + private Collection getOps(Class target, String name) { + Method method = ReflectionUtils.findMethod(target, name); + return source.getCacheOperations(method, target); } @Test public void testSingularAnnotation() throws Exception { - Collection ops = getOps("singular"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "singular", 1); assertTrue(ops.iterator().next() instanceof CacheableOperation); } @Test public void testMultipleAnnotation() throws Exception { - Collection ops = getOps("multiple"); - assertEquals(2, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "multiple", 2); Iterator it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); assertTrue(it.next() instanceof CacheEvictOperation); @@ -63,8 +74,7 @@ public class AnnotationCacheOperationSourceTests { @Test public void testCaching() throws Exception { - Collection ops = getOps("caching"); - assertEquals(2, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "caching", 2); Iterator it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); assertTrue(it.next() instanceof CacheEvictOperation); @@ -72,15 +82,13 @@ public class AnnotationCacheOperationSourceTests { @Test public void testSingularStereotype() throws Exception { - Collection ops = getOps("singleStereotype"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "singleStereotype", 1); assertTrue(ops.iterator().next() instanceof CacheEvictOperation); } @Test public void testMultipleStereotypes() throws Exception { - Collection ops = getOps("multipleStereotype"); - assertEquals(3, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "multipleStereotype", 3); Iterator it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); CacheOperation next = it.next(); @@ -93,16 +101,14 @@ public class AnnotationCacheOperationSourceTests { @Test public void testCustomKeyGenerator() { - Collection ops = getOps("customKeyGenerator"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "customKeyGenerator", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator()); } @Test public void testCustomKeyGeneratorInherited() { - Collection ops = getOps("customKeyGeneratorInherited"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "customKeyGeneratorInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator()); } @@ -110,7 +116,7 @@ public class AnnotationCacheOperationSourceTests { @Test public void testKeyAndKeyGeneratorCannotBeSetTogether() { try { - getOps("invalidKeyAndKeyGeneratorSet"); + getOps(AnnotatedClass.class, "invalidKeyAndKeyGeneratorSet"); fail("Should have failed to parse @Cacheable annotation"); } catch (IllegalStateException e) { // expected @@ -119,20 +125,70 @@ public class AnnotationCacheOperationSourceTests { @Test public void testCustomCacheManager() { - Collection ops = getOps("customCacheManager"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "customCacheManager", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache manager not set", "custom", cacheOperation.getCacheManager()); } @Test public void testCustomCacheManagerInherited() { - Collection ops = getOps("customCacheManagerInherited"); - assertEquals(1, ops.size()); + Collection ops = getOps(AnnotatedClass.class, "customCacheManagerInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache manager not set", "custom", cacheOperation.getCacheManager()); } + @Test + public void fullClassLevelWithCustomKeyManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classCacheManager", "custom", "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "custom", "classKeyGenerator", "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheName() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheName", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classCacheManager", "classKeyGenerator", "custom"); + } + + @Test + public void validateAtLeastOneCacheNameMustBeSet() { + thrown.expect(IllegalStateException.class); + getOps(AnnotatedClass.class, "noCacheNameSpecified"); + } + + @Test + public void customClassLevelWithCustomCacheName() { + Collection ops = getOps(AnnotatedClassWithCustomDefault.class, "methodLevelCacheName", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classCacheManager", "classKeyGenerator", "custom"); + } + + @Test + public void severalCacheConfigUseClosest() { + Collection ops = getOps(MultipleCacheConfig.class, "multipleCacheConfig"); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "", "", "myCache"); + } + + private void assertSharedConfig(CacheOperation actual, String cacheManager, + String keyGenerator, String... cacheNames) { + assertEquals("Wrong cache manager", cacheManager, actual.getCacheManager()); + assertEquals("Wrong key manager", keyGenerator, actual.getKeyGenerator()); + for (String cacheName : cacheNames) { + assertTrue("Cache '"+cacheName+"' not found (got "+actual.getCacheNames(), + actual.getCacheNames().contains(cacheName)); + } + assertEquals("Wrong number of cache name(s)", cacheNames.length, actual.getCacheNames().size()); + } + private static class AnnotatedClass { @Cacheable("test") public void singular() { @@ -180,6 +236,44 @@ public class AnnotationCacheOperationSourceTests { @CacheableFooCustomCacheManager public void customCacheManagerInherited() { } + + @Cacheable // cache name can be inherited from CacheConfig. There's none here + public void noCacheNameSpecified() { + } + } + + @CacheConfig(cacheNames = "classCacheName", + cacheManager = "classCacheManager", keyGenerator = "classKeyGenerator") + private static class AnnotatedClassWithFullDefault { + + @Cacheable(keyGenerator = "custom") + public void methodLevelKeyGenerator() { + } + + @Cacheable(cacheManager = "custom") + public void methodLevelCacheManager() { + } + + @Cacheable("custom") + public void methodLevelCacheName() { + } + } + + @CacheConfigFoo + private static class AnnotatedClassWithCustomDefault { + + @Cacheable("custom") + public void methodLevelCacheName() { + } + } + + @CacheConfigFoo + @CacheConfig(cacheNames = "myCache") // multiple sources + private static class MultipleCacheConfig { + + @Cacheable + public void multipleCacheConfig() { + } } @Retention(RetentionPolicy.RUNTIME) @@ -212,4 +306,10 @@ public class AnnotationCacheOperationSourceTests { @CacheEvict(value = "bar") public @interface EvictBar { } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @CacheConfig(cacheManager = "classCacheManager", keyGenerator = "classKeyGenerator") + public @interface CacheConfigFoo { + } } \ No newline at end of file