/* * Copyright 2002-2016 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.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.Collections; 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; import org.springframework.core.annotation.AliasFor; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; /** * @author Costin Leau * @author Stephane Nicoll * @author Sam Brannen */ public class AnnotationCacheOperationSourceTests { @Rule public final ExpectedException exception = ExpectedException.none(); private final AnnotationCacheOperationSource source = new AnnotationCacheOperationSource(); @Test public void singularAnnotation() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "singular", 1); assertTrue(ops.iterator().next() instanceof CacheableOperation); } @Test public void multipleAnnotation() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "multiple", 2); Iterator<CacheOperation> it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); assertTrue(it.next() instanceof CacheEvictOperation); } @Test public void caching() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "caching", 2); Iterator<CacheOperation> it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); assertTrue(it.next() instanceof CacheEvictOperation); } @Test public void emptyCaching() throws Exception { getOps(AnnotatedClass.class, "emptyCaching", 0); } @Test public void singularStereotype() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "singleStereotype", 1); assertTrue(ops.iterator().next() instanceof CacheEvictOperation); } @Test public void multipleStereotypes() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "multipleStereotype", 3); Iterator<CacheOperation> it = ops.iterator(); assertTrue(it.next() instanceof CacheableOperation); CacheOperation next = it.next(); assertTrue(next instanceof CacheEvictOperation); assertTrue(next.getCacheNames().contains("foo")); next = it.next(); assertTrue(next instanceof CacheEvictOperation); assertTrue(next.getCacheNames().contains("bar")); } @Test public void singleComposedAnnotation() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "singleComposed", 2); Iterator<CacheOperation> it = ops.iterator(); CacheOperation cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheableOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("directly declared"))); assertThat(cacheOperation.getKey(), equalTo("")); cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheableOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCache"))); assertThat(cacheOperation.getKey(), equalTo("composedKey")); } @Test public void multipleComposedAnnotations() throws Exception { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "multipleComposed", 4); Iterator<CacheOperation> it = ops.iterator(); CacheOperation cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheableOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("directly declared"))); assertThat(cacheOperation.getKey(), equalTo("")); cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheableOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCache"))); assertThat(cacheOperation.getKey(), equalTo("composedKey")); cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheableOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("foo"))); assertThat(cacheOperation.getKey(), equalTo("")); cacheOperation = it.next(); assertThat(cacheOperation, instanceOf(CacheEvictOperation.class)); assertThat(cacheOperation.getCacheNames(), equalTo(Collections.singleton("composedCacheEvict"))); assertThat(cacheOperation.getKey(), equalTo("composedEvictionKey")); } @Test public void customKeyGenerator() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customKeyGenerator", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator()); } @Test public void customKeyGeneratorInherited() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customKeyGeneratorInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom key generator not set", "custom", cacheOperation.getKeyGenerator()); } @Test public void keyAndKeyGeneratorCannotBeSetTogether() { this.exception.expect(IllegalStateException.class); getOps(AnnotatedClass.class, "invalidKeyAndKeyGeneratorSet"); } @Test public void customCacheManager() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customCacheManager", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache manager not set", "custom", cacheOperation.getCacheManager()); } @Test public void customCacheManagerInherited() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customCacheManagerInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache manager not set", "custom", cacheOperation.getCacheManager()); } @Test public void customCacheResolver() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customCacheResolver", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache resolver not set", "custom", cacheOperation.getCacheResolver()); } @Test public void customCacheResolverInherited() { Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "customCacheResolverInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); assertEquals("Custom cache resolver not set", "custom", cacheOperation.getCacheResolver()); } @Test public void cacheResolverAndCacheManagerCannotBeSetTogether() { this.exception.expect(IllegalStateException.class); getOps(AnnotatedClass.class, "invalidCacheResolverAndCacheManagerSet"); } @Test public void fullClassLevelWithCustomCacheName() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheName", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); } @Test public void fullClassLevelWithCustomKeyManager() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "custom", "", "classCacheResolver" , "classCacheName"); } @Test public void fullClassLevelWithCustomCacheManager() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); } @Test public void fullClassLevelWithCustomCacheResolver() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheResolver", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom" , "classCacheName"); } @Test public void validateNoCacheIsValid() { // Valid as a CacheResolver might return the cache names to use with other info Collection<CacheOperation> ops = getOps(AnnotatedClass.class, "noCacheNameSpecified"); CacheOperation cacheOperation = ops.iterator().next(); assertNotNull("cache names set must not be null", cacheOperation.getCacheNames()); assertEquals("no cache names specified", 0, cacheOperation.getCacheNames().size()); } @Test public void customClassLevelWithCustomCacheName() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithCustomDefault.class, "methodLevelCacheName", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); } @Test public void severalCacheConfigUseClosest() { Collection<CacheOperation> ops = getOps(MultipleCacheConfig.class, "multipleCacheConfig"); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "", "", "", "myCache"); } @Test public void cacheConfigFromInterface() { Collection<CacheOperation> ops = getOps(InterfaceCacheConfig.class, "interfaceCacheConfig"); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "", "", "", "myCache"); } @Test public void cacheAnnotationOverride() { Collection<CacheOperation> ops = getOps(InterfaceCacheConfig.class, "interfaceCacheableOverride"); assertSame(1, ops.size()); CacheOperation cacheOperation = ops.iterator().next(); assertTrue(cacheOperation instanceof CacheableOperation); } @Test public void partialClassLevelWithCustomCacheManager() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheManager", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); } @Test public void partialClassLevelWithCustomCacheResolver() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheResolver", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom", "classCacheName"); } @Test public void partialClassLevelWithNoCustomization() { Collection<CacheOperation> ops = getOps(AnnotatedClassWithSomeDefault.class, "noCustomization", 1); CacheOperation cacheOperation = ops.iterator().next(); assertSharedConfig(cacheOperation, "classKeyGenerator", "classCacheManager", "", "classCacheName"); } private Collection<CacheOperation> getOps(Class<?> target, String name, int expectedNumberOfOperations) { Collection<CacheOperation> result = getOps(target, name); assertEquals("Wrong number of operation(s) for '" + name + "'", expectedNumberOfOperations, result.size()); return result; } private Collection<CacheOperation> getOps(Class<?> target, String name) { try { Method method = target.getMethod(name); return this.source.getCacheOperations(method, target); } catch (NoSuchMethodException ex) { throw new IllegalStateException(ex); } } private void assertSharedConfig(CacheOperation actual, String keyGenerator, String cacheManager, String cacheResolver, String... cacheNames) { assertEquals("Wrong key manager", keyGenerator, actual.getKeyGenerator()); assertEquals("Wrong cache manager", cacheManager, actual.getCacheManager()); assertEquals("Wrong cache resolver", cacheResolver, actual.getCacheResolver()); assertEquals("Wrong number of cache names", cacheNames.length, actual.getCacheNames().size()); Arrays.stream(cacheNames).forEach(cacheName -> assertTrue("Cache '" + cacheName + "' not found in " + actual.getCacheNames(), actual.getCacheNames().contains(cacheName))); } private static class AnnotatedClass { @Cacheable("test") public void singular() { } @CacheEvict("test") @Cacheable("test") public void multiple() { } @Caching(cacheable = @Cacheable("test"), evict = @CacheEvict("test")) public void caching() { } @Caching public void emptyCaching() { } @Cacheable(cacheNames = "test", keyGenerator = "custom") public void customKeyGenerator() { } @Cacheable(cacheNames = "test", cacheManager = "custom") public void customCacheManager() { } @Cacheable(cacheNames = "test", cacheResolver = "custom") public void customCacheResolver() { } @EvictFoo public void singleStereotype() { } @EvictFoo @CacheableFoo @EvictBar public void multipleStereotype() { } @Cacheable("directly declared") @ComposedCacheable(cacheNames = "composedCache", key = "composedKey") public void singleComposed() { } @Cacheable("directly declared") @ComposedCacheable(cacheNames = "composedCache", key = "composedKey") @CacheableFoo @ComposedCacheEvict(cacheNames = "composedCacheEvict", key = "composedEvictionKey") public void multipleComposed() { } @Caching(cacheable = { @Cacheable(cacheNames = "test", key = "a"), @Cacheable(cacheNames = "test", key = "b") }) public void multipleCaching() { } @CacheableFooCustomKeyGenerator public void customKeyGeneratorInherited() { } @Cacheable(cacheNames = "test", key = "#root.methodName", keyGenerator = "custom") public void invalidKeyAndKeyGeneratorSet() { } @CacheableFooCustomCacheManager public void customCacheManagerInherited() { } @CacheableFooCustomCacheResolver public void customCacheResolverInherited() { } @Cacheable(cacheNames = "test", cacheManager = "custom", cacheResolver = "custom") public void invalidCacheResolverAndCacheManagerSet() { } @Cacheable // cache name can be inherited from CacheConfig. There's none here public void noCacheNameSpecified() { } } @CacheConfig(cacheNames = "classCacheName", keyGenerator = "classKeyGenerator", cacheManager = "classCacheManager", cacheResolver = "classCacheResolver") private static class AnnotatedClassWithFullDefault { @Cacheable("custom") public void methodLevelCacheName() { } @Cacheable(keyGenerator = "custom") public void methodLevelKeyGenerator() { } @Cacheable(cacheManager = "custom") public void methodLevelCacheManager() { } @Cacheable(cacheResolver = "custom") public void methodLevelCacheResolver() { } } @CacheConfigFoo private static class AnnotatedClassWithCustomDefault { @Cacheable("custom") public void methodLevelCacheName() { } } @CacheConfig(cacheNames = "classCacheName", keyGenerator = "classKeyGenerator", cacheManager = "classCacheManager") private static class AnnotatedClassWithSomeDefault { @Cacheable(cacheManager = "custom") public void methodLevelCacheManager() { } @Cacheable(cacheResolver = "custom") public void methodLevelCacheResolver() { } @Cacheable public void noCustomization() { } } @CacheConfigFoo @CacheConfig(cacheNames = "myCache") // multiple sources private static class MultipleCacheConfig { @Cacheable public void multipleCacheConfig() { } } @CacheConfig(cacheNames = "myCache") private interface CacheConfigIfc { @Cacheable void interfaceCacheConfig(); @CachePut void interfaceCacheableOverride(); } private static class InterfaceCacheConfig implements CacheConfigIfc { @Override public void interfaceCacheConfig() { } @Override @Cacheable public void interfaceCacheableOverride() { } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Cacheable("foo") public @interface CacheableFoo { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Cacheable(cacheNames = "foo", keyGenerator = "custom") public @interface CacheableFooCustomKeyGenerator { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Cacheable(cacheNames = "foo", cacheManager = "custom") public @interface CacheableFooCustomCacheManager { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Cacheable(cacheNames = "foo", cacheResolver = "custom") public @interface CacheableFooCustomCacheResolver { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @CacheEvict("foo") public @interface EvictFoo { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @CacheEvict("bar") public @interface EvictBar { } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @CacheConfig(keyGenerator = "classKeyGenerator", cacheManager = "classCacheManager", cacheResolver = "classCacheResolver") public @interface CacheConfigFoo { } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @Cacheable(cacheNames = "shadowed cache name", key = "shadowed key") @interface ComposedCacheable { @AliasFor(annotation = Cacheable.class) String[] value() default {}; @AliasFor(annotation = Cacheable.class) String[] cacheNames() default {}; @AliasFor(annotation = Cacheable.class) String key() default ""; } @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @CacheEvict(cacheNames = "shadowed cache name", key = "shadowed key") @interface ComposedCacheEvict { @AliasFor(annotation = CacheEvict.class) String[] value() default {}; @AliasFor(annotation = CacheEvict.class) String[] cacheNames() default {}; @AliasFor(annotation = CacheEvict.class) String key() default ""; } }