diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj index 256e609f996..61b93e0551e 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj @@ -20,6 +20,7 @@ import java.lang.reflect.Method; import org.aspectj.lang.annotation.SuppressAjWarnings; import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.beans.factory.DisposableBean; import org.springframework.cache.interceptor.CacheAspectSupport; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.interceptor.CacheOperationSource; @@ -36,9 +37,10 @@ import org.springframework.cache.interceptor.CacheOperationSource; * relevant Spring cache definition will not be resolved. * * @author Costin Leau + * @author Stephane Nicoll * @since 3.1 */ -public abstract aspect AbstractCacheAspect extends CacheAspectSupport { +public abstract aspect AbstractCacheAspect extends CacheAspectSupport implements DisposableBean { protected AbstractCacheAspect() { } @@ -52,6 +54,11 @@ public abstract aspect AbstractCacheAspect extends CacheAspectSupport { setCacheOperationSources(cos); } + @Override + public void destroy() { + clearMetadataCache(); // An aspect is basically a singleton + } + @SuppressAjWarnings("adviceDidNotMatch") Object around(final Object cachedObject) : cacheMethodExecution(cachedObject) { MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java index 1b9e5873c50..5c7bb93d082 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 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. @@ -25,7 +25,7 @@ import org.junit.Test; import org.springframework.cache.Cache; import org.springframework.cache.config.AbstractAnnotationTests; import org.springframework.cache.config.CacheableService; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; /** @@ -35,7 +35,7 @@ public class AspectJAnnotationTests extends AbstractAnnotationTests { @Override - protected ApplicationContext getApplicationContext() { + protected ConfigurableApplicationContext getApplicationContext() { return new GenericXmlApplicationContext("/org/springframework/cache/config/annotation-cache-aspectj.xml"); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java index 9ba0ae59da0..af2ebfb02d0 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java @@ -36,13 +36,11 @@ import org.springframework.context.annotation.Role; @Configuration public class AbstractJCacheConfiguration extends AbstractCachingConfiguration { - protected CacheResolver cacheResolver; protected CacheResolver exceptionCacheResolver; @Override protected void useCachingConfigurer(JCacheConfigurer config) { super.useCachingConfigurer(config); - this.cacheResolver = config.cacheResolver(); this.exceptionCacheResolver = config.exceptionCacheResolver(); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java index cbbde55d95f..821d2e6b2e9 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java @@ -21,42 +21,25 @@ import org.springframework.cache.interceptor.CacheResolver; /** * Extension of {@link CachingConfigurer} for the JSR-107 implementation. - - *

To be implemented by classes annotated with @{@link org.springframework.cache.annotation.EnableCaching} - * that wish or need to specify explicitly the {@link CacheResolver} bean(s) to be used for - * annotation-driven cache management. * - *

See @{@link org.springframework.cache.annotation.EnableCaching} for general examples and - * context; see {@link #cacheResolver()} and {@link #exceptionCacheResolver()} for detailed + *

To be implemented by classes annotated with + * @{@link org.springframework.cache.annotation.EnableCaching} that wish or + * need to specify explicitly how exception caches are resolved for + * annotation-driven cache management. Consider extending {@link JCacheConfigurerSupport}, + * which provides a stub implementation of all interface methods. + * + *

See @{@link org.springframework.cache.annotation.EnableCaching} for + * general examples and context; see {@link #exceptionCacheResolver()} for detailed * instructions. * * @author Stephane Nicoll * @since 4.1 * @see CachingConfigurer + * @see JCacheConfigurerSupport * @see org.springframework.cache.annotation.EnableCaching */ public interface JCacheConfigurer extends CachingConfigurer { - /** - * Return the {@link CacheResolver} bean to use to resolve regular caches for - * annotation-driven cache management. Implementations must explicitly declare - * {@link org.springframework.context.annotation.Bean @Bean}, e.g. - *

-	 * @Configuration
-	 * @EnableCaching
-	 * public class AppConfig implements JCacheConfigurer {
-	 *     @Bean // important!
-	 *     @Override
-	 *     public CacheResolver cacheResolver() {
-	 *         // configure and return CacheResolver instance
-	 *     }
-	 *     // ...
-	 * }
-	 * 
- * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples. - */ - CacheResolver cacheResolver(); - /** * Return the {@link CacheResolver} bean to use to resolve exception caches for * annotation-driven cache management. Implementations must explicitly declare @@ -64,7 +47,7 @@ public interface JCacheConfigurer extends CachingConfigurer { *
 	 * @Configuration
 	 * @EnableCaching
-	 * public class AppConfig implements JCacheConfigurer {
+	 * public class AppConfig extends JCacheConfigurerSupport {
 	 *     @Bean // important!
 	 *     @Override
 	 *     public CacheResolver exceptionCacheResolver() {
diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java
new file mode 100644
index 00000000000..cd6adc7ef8f
--- /dev/null
+++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java
@@ -0,0 +1,41 @@
+/*
+ * 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.jcache.config;
+
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.interceptor.CacheResolver;
+
+/**
+ * An extension of {@link CachingConfigurerSupport} that also implements
+ * {@link JCacheConfigurer}.
+ *
+ * 

Users of JSR-107 annotations may extend from this class rather than + * implementing from {@link JCacheConfigurer} directly. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JCacheConfigurer + * @see CachingConfigurerSupport + */ +public class JCacheConfigurerSupport extends CachingConfigurerSupport implements JCacheConfigurer { + + @Override + public CacheResolver exceptionCacheResolver() { + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java index edcab3a14d3..0809e13bc40 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -19,13 +19,12 @@ package org.springframework.cache.jcache.interceptor; import java.util.Collection; import java.util.Collections; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.BaseCacheResolver; import org.springframework.cache.interceptor.BasicCacheOperation; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.jcache.model.CacheResultOperation; -import org.springframework.util.Assert; /** * A simple {@link CacheResolver} that resolves the exception cache @@ -36,16 +35,14 @@ import org.springframework.util.Assert; * @since 4.1 * @see org.springframework.cache.jcache.model.CacheResultOperation#getExceptionCacheName() */ -public class SimpleExceptionCacheResolver implements CacheResolver { - - private final CacheManager cacheManager; +public class SimpleExceptionCacheResolver extends BaseCacheResolver { public SimpleExceptionCacheResolver(CacheManager cacheManager) { - this.cacheManager = cacheManager; + super(cacheManager); } @Override - public Collection resolveCaches(CacheOperationInvocationContext context) { + protected Collection getCacheNames(CacheOperationInvocationContext context) { BasicCacheOperation operation = context.getOperation(); if (!(operation instanceof CacheResultOperation)) { throw new IllegalStateException("Could not extract exception cache name from " + operation); @@ -53,9 +50,7 @@ public class SimpleExceptionCacheResolver implements CacheResolver { CacheResultOperation cacheResultOperation = (CacheResultOperation) operation; String exceptionCacheName = cacheResultOperation.getExceptionCacheName(); if (exceptionCacheName != null) { - Cache cache = cacheManager.getCache(exceptionCacheName); - Assert.notNull(cache, "Cannot find cache named '" + exceptionCacheName + "' for " + operation); - return Collections.singleton(cache); + return Collections.singleton(exceptionCacheName); } return null; } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java index a15b8771149..8bf177b5314 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java @@ -26,15 +26,19 @@ import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.config.SomeKeyGenerator; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; import org.springframework.cache.interceptor.SimpleCacheResolver; import org.springframework.cache.interceptor.SimpleKeyGenerator; import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService; import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.jcache.interceptor.SimpleExceptionCacheResolver; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -61,6 +65,35 @@ public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { cos.getDefaultExceptionCacheResolver()); } + @Test + public void emptyConfigSupport() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); + + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertNotNull(cos.getDefaultCacheResolver()); + assertEquals(SimpleCacheResolver.class, cos.getDefaultCacheResolver().getClass()); + assertSame(context.getBean(CacheManager.class), + ((SimpleCacheResolver) cos.getDefaultCacheResolver()).getCacheManager()); + assertNotNull(cos.getDefaultExceptionCacheResolver()); + assertEquals(SimpleExceptionCacheResolver.class, cos.getDefaultExceptionCacheResolver().getClass()); + assertSame(context.getBean(CacheManager.class), + ((SimpleExceptionCacheResolver) cos.getDefaultExceptionCacheResolver()).getCacheManager()); + context.close(); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(FullCachingConfigSupport.class); + + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertSame(context.getBean("cacheResolver"), cos.getDefaultCacheResolver()); + assertSame(context.getBean("keyGenerator"), cos.getDefaultKeyGenerator()); + assertSame(context.getBean("exceptionCacheResolver"), cos.getDefaultExceptionCacheResolver()); + context.close(); + } + @Configuration @EnableCaching @@ -118,4 +151,42 @@ public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { } } + @Configuration + @EnableCaching + public static class EmptyConfigSupportConfig extends JCacheConfigurerSupport { + @Bean + public CacheManager cm() { + return new NoOpCacheManager(); + } + } + + @Configuration + @EnableCaching + static class FullCachingConfigSupport extends JCacheConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(cacheManager(), "foo"); + } + + @Override + @Bean + public CacheResolver exceptionCacheResolver() { + return new NamedCacheResolver(cacheManager(), "exception"); + } + } + } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java index 92b2f38d549..1b980c08ca2 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java @@ -19,18 +19,14 @@ package org.springframework.cache.jcache.interceptor; import static org.junit.Assert.*; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; import org.junit.Test; -import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; import org.springframework.cache.jcache.AbstractJCacheTests; import org.springframework.context.support.StaticApplicationContext; import org.springframework.util.ReflectionUtils; @@ -45,7 +41,7 @@ public class JCacheInterceptorTests extends AbstractJCacheTests { @Test public void severalCachesNotSupported() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( - cacheManager, new TestCacheResolver("default", "exception"), + cacheManager, new NamedCacheResolver(cacheManager, "default", "simpleCache"), defaultExceptionCacheResolver, defaultKeyGenerator)); AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); @@ -65,7 +61,7 @@ public class JCacheInterceptorTests extends AbstractJCacheTests { @Test public void noCacheCouldBeResolved() { JCacheInterceptor interceptor = createInterceptor(createOperationSource( - cacheManager, new TestCacheResolver(), // Returns empty list + cacheManager, new NamedCacheResolver(cacheManager), // Returns empty list defaultExceptionCacheResolver, defaultKeyGenerator)); AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); @@ -132,25 +128,6 @@ public class JCacheInterceptorTests extends AbstractJCacheTests { return interceptor; } - - private class TestCacheResolver implements CacheResolver { - - private final String[] cacheNames; - - private TestCacheResolver(String... cacheNames) { - this.cacheNames = cacheNames; - } - - @Override - public Collection resolveCaches(CacheOperationInvocationContext context) { - List result = new ArrayList(); - for (String cacheName : cacheNames) { - result.add(cacheManager.getCache(cacheName)); - } - return result; - } - } - private static class DummyInvoker implements CacheOperationInvoker { private final Object result; diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java index c9f88bd7787..47efe817da9 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java @@ -22,6 +22,7 @@ import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportAware; @@ -46,6 +47,8 @@ public abstract class AbstractCachingConfiguration protected CacheManager cacheManager; + protected CacheResolver cacheResolver; + protected KeyGenerator keyGenerator; @Autowired(required=false) @@ -86,7 +89,7 @@ public abstract class AbstractCachingConfiguration C cachingConfigurer = cachingConfigurers.iterator().next(); useCachingConfigurer(cachingConfigurer); } - else if (!CollectionUtils.isEmpty(cacheManagerBeans)) { + if (this.cacheManager == null && !CollectionUtils.isEmpty(cacheManagerBeans)) { int nManagers = cacheManagerBeans.size(); if (nManagers > 1) { throw new IllegalStateException(nManagers + " beans of type CacheManager " + @@ -98,7 +101,7 @@ public abstract class AbstractCachingConfiguration this.cacheManager = cacheManagerBeans.iterator().next(); // keyGenerator remains null; will fall back to default within CacheInterceptor } - else { + if (this.cacheManager == null) { throw new IllegalStateException("No bean of type CacheManager could be found. " + "Register a CacheManager bean or remove the @EnableCaching annotation " + "from your configuration."); @@ -110,6 +113,7 @@ public abstract class AbstractCachingConfiguration */ protected void useCachingConfigurer(C config) { this.cacheManager = config.cacheManager(); + this.cacheResolver = config.cacheResolver(); this.keyGenerator = config.keyGenerator(); } 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 index 0c30d960253..6446e5ae47c 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheConfig.java @@ -53,10 +53,21 @@ public @interface CacheConfig { 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. + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

If no resolver and no cache manager are set at the operation level, and no cache + * resolver is set on this instance, this one is used instead of the default. + * @see org.springframework.cache.interceptor.SimpleCacheResolver */ String cacheManager() default ""; + + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. + *

If no resolver and no cache manager are set at the operation level, this one is used + * instead of the default. + */ + String cacheResolver() 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 5a4529b8e77..fcc3e443908 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 @@ -59,10 +59,19 @@ public @interface CacheEvict { String keyGenerator() default ""; /** - * The bean name of the custom {@link org.springframework.cache.CacheManager} to use. + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

Mutually exclusive with the {@link #cacheResolver()} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver */ String cacheManager() default ""; + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. + */ + String cacheResolver() default ""; + /** * Spring Expression Language (SpEL) attribute used for conditioning the method caching. *

Default is "", meaning the method is always cached. 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 81f7f463bd2..13b94c4f8f2 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 @@ -64,10 +64,19 @@ public @interface CachePut { String keyGenerator() default ""; /** - * The bean name of the custom {@link org.springframework.cache.CacheManager} to use. + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

Mutually exclusive with the {@link #cacheResolver()} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver */ String cacheManager() default ""; + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. + */ + String cacheResolver() default ""; + /** * Spring Expression Language (SpEL) attribute used for conditioning the cache update. *

Default is "", meaning the method result is always cached. 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 64eefc14633..3e871086b76 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 @@ -62,10 +62,19 @@ public @interface Cacheable { String keyGenerator() default ""; /** - * The bean name of the custom {@link org.springframework.cache.CacheManager} to use. + * The bean name of the custom {@link org.springframework.cache.CacheManager} to use to + * create a default {@link org.springframework.cache.interceptor.CacheResolver} if none + * is set already. + *

Mutually exclusive with the {@link #cacheResolver()} attribute. + * @see org.springframework.cache.interceptor.SimpleCacheResolver */ String cacheManager() default ""; + /** + * The bean name of the custom {@link org.springframework.cache.interceptor.CacheResolver} to use. + */ + String cacheResolver() default ""; + /** * Spring Expression Language (SpEL) attribute used for conditioning the method caching. *

Default is "", meaning the method is always cached. diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java index b3dd6224c59..524e9ad901e 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 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,31 +17,39 @@ package org.springframework.cache.annotation; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; /** * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration * Configuration} classes annotated with @{@link EnableCaching} that wish or need to - * specify explicitly the {@link CacheManager} and {@link KeyGenerator} beans to be used - * for annotation-driven cache management. + * specify explicitly how caches are resolved and how keys are generated for annotation-driven + * cache management. Consider extending {@link CachingConfigurerSupport}, which provides a + * stub implementation of all interface methods. * *

See @{@link EnableCaching} for general examples and context; see - * {@link #cacheManager()} and {@link #keyGenerator()} for detailed instructions. + * {@link #cacheManager()}, {@link #cacheResolver()} and {@link #keyGenerator()} + * for detailed instructions. * * @author Chris Beams * @since 3.1 * @see EnableCaching + * @see CachingConfigurerSupport */ public interface CachingConfigurer { /** - * Return the cache manager bean to use for annotation-driven cache management. - * Implementations must explicitly declare + * Return the cache manager bean to use for annotation-driven cache + * management. A default {@link CacheResolver} will be initialized + * behind the scene with this cache manager. For more fine-grained + * management of the cache resolution, consider setting the + * {@link CacheResolver} directly. + *

Implementations must explicitly declare * {@link org.springframework.context.annotation.Bean @Bean}, e.g. *

 	 * @Configuration
 	 * @EnableCaching
-	 * public class AppConfig implements CachingConfigurer {
+	 * public class AppConfig extends CachingConfigurerSupport {
 	 *     @Bean // important!
 	 *     @Override
 	 *     public CacheManager cacheManager() {
@@ -54,6 +62,28 @@ public interface CachingConfigurer {
 	 */
 	CacheManager cacheManager();
 
+	/**
+	 * Return the {@link CacheResolver} bean to use to resolve regular caches for
+	 * annotation-driven cache management. This is an alternative option to set
+	 * the {@link CacheManager} to use.
+	 * 

Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

+	 * @Configuration
+	 * @EnableCaching
+	 * public class AppConfig extends CachingConfigurerSupport {
+	 *     @Bean // important!
+	 *     @Override
+	 *     public CacheResolver cacheResolver() {
+	 *         // configure and return CacheResolver instance
+	 *     }
+	 *     // ...
+	 * }
+	 * 
+ * See {@link EnableCaching} for more complete examples. + */ + CacheResolver cacheResolver(); + /** * Return the key generator bean to use for annotation-driven cache management. * Implementations must explicitly declare @@ -61,7 +91,7 @@ public interface CachingConfigurer { *
 	 * @Configuration
 	 * @EnableCaching
-	 * public class AppConfig implements CachingConfigurer {
+	 * public class AppConfig extends CachingConfigurerSupport {
 	 *     @Bean // important!
 	 *     @Override
 	 *     public KeyGenerator keyGenerator() {
diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java
new file mode 100644
index 00000000000..29801d6633c
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java
@@ -0,0 +1,48 @@
+/*
+ * 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 org.springframework.cache.CacheManager;
+import org.springframework.cache.interceptor.CacheResolver;
+import org.springframework.cache.interceptor.KeyGenerator;
+
+/**
+ * An implementation of {@link CachingConfigurer} with empty methods allowing
+ * sub-classes to override only the methods they're interested in.
+ *
+ * @author Stephane Nicoll
+ * @since 4.1
+ * @see CachingConfigurer
+ */
+public class CachingConfigurerSupport implements CachingConfigurer {
+
+	@Override
+	public CacheManager cacheManager() {
+		return null;
+	}
+
+	@Override
+	public KeyGenerator keyGenerator() {
+		return null;
+	}
+
+	@Override
+	public CacheResolver cacheResolver() {
+		return null;
+	}
+
+}
diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java
index 1e1fbc52d09..aeb03c368ed 100644
--- a/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java
+++ b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java
@@ -59,7 +59,10 @@ public class ProxyCachingConfiguration extends AbstractCachingConfiguration target) {
 		final CacheConfig annotation = AnnotationUtils.getAnnotation(target, CacheConfig.class);
 		if (annotation != null) {
-			return new DefaultCacheConfig(annotation.cacheManager(),
-					annotation.keyGenerator(), annotation.cacheNames());
+			return new DefaultCacheConfig(annotation.cacheNames(), annotation.keyGenerator(),
+					annotation.cacheManager(), annotation.cacheResolver());
 		}
 		return new DefaultCacheConfig();
 	}
@@ -228,9 +231,16 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
 					"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 (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
+			throw new IllegalStateException("Invalid cache annotation configuration on '"
+					+ ae.toString() + "'. Both 'cacheManager' and 'cacheResolver' attributes have been set. " +
+					"These attributes are mutually exclusive: the cache manager is used to configure a" +
+					"default cache resolver if none is set. If a cache resolver is set, the cache manager" +
+					"won't be used.");
+		}
 		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" +
+					+ 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.");
 		}
 	}
@@ -245,22 +255,29 @@ 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;
+		private final String keyGenerator;
+
+		private final String cacheManager;
+
+		private final String cacheResolver;
+
+		private DefaultCacheConfig(String[] cacheNames, String keyGenerator,
+				String cacheManager, String cacheResolver) {
 			this.cacheNames = cacheNames;
+			this.keyGenerator = keyGenerator;
+			this.cacheManager = cacheManager;
+			this.cacheResolver = cacheResolver;
 		}
 
 		public DefaultCacheConfig() {
-			this(null, null, null);
+			this(null, null, null, null);
 		}
 
 		/**
@@ -269,17 +286,29 @@ public class SpringCacheAnnotationParser implements CacheAnnotationParser, Seria
 		 * @param operation the operation to update
 		 */
 		public void applyDefault(CacheOperation operation) {
-			if (!StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(cacheManager)) {
-				operation.setCacheManager(cacheManager);
+			if (operation.getCacheNames().isEmpty() && cacheNames != null) {
+				operation.setCacheNames(cacheNames);
 			}
 			if (!StringUtils.hasText(operation.getKey()) && !StringUtils.hasText(operation.getKeyGenerator())
 					&& StringUtils.hasText(keyGenerator)) {
 				operation.setKeyGenerator(keyGenerator);
 			}
-			if (operation.getCacheNames().isEmpty() && cacheNames != null) {
-				operation.setCacheNames(cacheNames);
+
+			if (isSet(operation.getCacheManager()) || isSet(operation.getCacheResolver())) {
+				// One of these is set so we should not inherit anything
+			}
+			else if (isSet(cacheResolver)) {
+				operation.setCacheResolver(cacheResolver);
+			}
+			else if (isSet(cacheManager)) {
+				operation.setCacheManager(cacheManager);
 			}
 		}
+
+		private boolean isSet(String s) {
+			return StringUtils.hasText(s);
+		}
+
 	}
 
 }
diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/BaseCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/BaseCacheResolver.java
new file mode 100644
index 00000000000..43010a73d2b
--- /dev/null
+++ b/spring-context/src/main/java/org/springframework/cache/interceptor/BaseCacheResolver.java
@@ -0,0 +1,92 @@
+/*
+ * 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.interceptor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.util.Assert;
+
+/**
+ * A base {@link CacheResolver} implementation that requires the concrete
+ * implementation to provide the collection of cache name(s) based on the
+ * invocation context.
+ *
+ * @author Stephane Nicoll
+ */
+public abstract class BaseCacheResolver implements CacheResolver, InitializingBean {
+
+	private CacheManager cacheManager;
+
+	protected BaseCacheResolver(CacheManager cacheManager) {
+		this.cacheManager = cacheManager;
+	}
+
+	protected BaseCacheResolver() {
+	}
+
+	/**
+	 * Set the {@link CacheManager} that this instance should use.
+	 */
+	public void setCacheManager(CacheManager cacheManager) {
+		this.cacheManager = cacheManager;
+	}
+
+	/**
+	 * Return the {@link CacheManager} that this instance use.
+	 */
+	public CacheManager getCacheManager() {
+		return cacheManager;
+	}
+
+	@Override
+	public void afterPropertiesSet()  {
+		Assert.notNull(cacheManager, "CacheManager must not be null");
+	}
+
+	@Override
+	public Collection resolveCaches(CacheOperationInvocationContext context) {
+		Collection cacheNames = getCacheNames(context);
+		if (cacheNames == null) {
+			return Collections.emptyList();
+		}
+		else {
+			Collection result = new ArrayList();
+			for (String cacheName : cacheNames) {
+				Cache cache = cacheManager.getCache(cacheName);
+				Assert.notNull(cache, "Cannot find cache named '" + cacheName + "' for " + context.getOperation());
+				result.add(cache);
+			}
+			return result;
+		}
+	}
+
+	/**
+	 * Provide the name of the cache(s) to resolve against the current cache manager.
+	 * 

It is acceptable to return {@code null} to indicate that no cache could + * be resolved for this invocation. + * + * @param context the context of the particular invocation + * @return the cache name(s) to resolve or {@code null} if no cache should be resolved + */ + protected abstract Collection getCacheNames(CacheOperationInvocationContext context); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 0f04f3f19dc..6fa24bf3388 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -17,16 +17,15 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Set; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; @@ -54,12 +53,12 @@ import org.springframework.util.StringUtils; *

Subclasses are responsible for calling methods in this class in * the correct order. * - *

Uses the Strategy design pattern. A {@link CacheManager} - * implementation will perform the actual cache management, and a + *

Uses the Strategy design pattern. A {@link CacheResolver} + * implementation will resolve the actual cache(s) to use, and a * {@link CacheOperationSource} is used for determining caching * operations. * - *

A cache aspect is serializable if its {@code CacheManager} and + *

A cache aspect is serializable if its {@code CacheResolver} and * {@code CacheOperationSource} are serializable. * * @author Costin Leau @@ -74,34 +73,25 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio protected final Log logger = LogFactory.getLog(getClass()); - private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); + /** + * Cache of CacheOperationMetadata, keyed by {@link CacheOperationCacheKey}. + */ + private final Map metadataCache = + new ConcurrentHashMap(1024); - private CacheManager cacheManager; + private final ExpressionEvaluator evaluator = new ExpressionEvaluator(); private CacheOperationSource cacheOperationSource; private KeyGenerator keyGenerator = new SimpleKeyGenerator(); + private CacheResolver cacheResolver; + private ApplicationContext applicationContext; private boolean initialized = false; - /** - * Set the default {@link CacheManager} that this cache aspect should delegate to - * if no specific cache manager has been set for the operation. - */ - public void setCacheManager(CacheManager cacheManager) { - this.cacheManager = cacheManager; - } - - /** - * Return the default {@link CacheManager} that this cache aspect delegates to. - */ - public CacheManager getCacheManager() { - return this.cacheManager; - } - /** * Set one or more cache operation sources which are used to find the cache * attributes. If more than one source is provided, they will be aggregated using a @@ -137,14 +127,46 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio return this.keyGenerator; } + /** + * Set the {@link CacheManager} to use to create a default {@link CacheResolver}. Replace + * the current {@link CacheResolver}, if any. + * + * @see #setCacheResolver(CacheResolver) + * @see SimpleCacheResolver + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheResolver = new SimpleCacheResolver(cacheManager); + } + + /** + * Set the default {@link CacheResolver} that this cache aspect should delegate + * to if no specific cache resolver has been set for the operation. + *

The default resolver resolves the caches against their names and the + * default cache manager. + * @see #setCacheManager(org.springframework.cache.CacheManager) + * @see SimpleCacheResolver + */ + public void setCacheResolver(CacheResolver cacheResolver) { + Assert.notNull(cacheResolver); + this.cacheResolver = cacheResolver; + } + + /** + * Return the default {@link CacheResolver} that this cache aspect delegates to. + */ + public CacheResolver getCacheResolver() { + return cacheResolver; + } + @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } public void afterPropertiesSet() { - Assert.state(this.cacheManager != null, "Property 'cacheManager' is required"); - Assert.state(this.cacheOperationSource != null, "Property 'cacheOperationSources' is required: " + + Assert.state(this.cacheResolver != null, "'cacheResolver' is required. Either set the cache resolver " + + "to use or set the cache manager to create a default cache resolver based on it."); + Assert.state(this.cacheOperationSource != null, "The 'cacheOperationSources' property is required: " + "If there are no cacheable methods, then don't use a cache aspect."); Assert.state(this.applicationContext != null, "The application context was not injected as it should."); this.initialized = true; @@ -165,21 +187,85 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio return ClassUtils.getQualifiedMethodName(specificMethod); } - protected Collection getCaches(CacheOperation operation, CacheManager cacheManager) { - Set cacheNames = operation.getCacheNames(); - Collection caches = new ArrayList(cacheNames.size()); - for (String cacheName : cacheNames) { - Cache cache = cacheManager.getCache(cacheName); - Assert.notNull(cache, "Cannot find cache named '" + cacheName + "' for " + operation); - caches.add(cache); + protected Collection getCaches(CacheOperationInvocationContext context, + CacheResolver cacheResolver) { + Collection caches = cacheResolver.resolveCaches(context); + if (caches.isEmpty()) { + throw new IllegalStateException("No cache could be resolved for '" + context.getOperation() + + "' using resolver '" + cacheResolver + + "'. At least one cache should be provided per cache operation."); } return caches; } protected CacheOperationContext getOperationContext(CacheOperation operation, Method method, Object[] args, - Object target, Class targetClass) { + Object target, Class targetClass) { - return new CacheOperationContext(operation, method, args, target, targetClass); + CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass); + return new CacheOperationContext(metadata, args, target); + } + + /** + * Return the {@link CacheOperationMetadata} for the specified operation. + *

Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be + * used for the operation. + * @param operation the operation + * @param method the method on which the operation is invoked + * @param targetClass the target type + * @return the resolved metadata for the operation + */ + protected CacheOperationMetadata getCacheOperationMetadata(CacheOperation operation, + Method method, Class targetClass) { + final CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass); + CacheOperationMetadata metadata = metadataCache.get(cacheKey); + if (metadata == null) { + KeyGenerator operationKeyGenerator; + if (StringUtils.hasText(operation.getKeyGenerator())) { + operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class); + } + else { + operationKeyGenerator = getKeyGenerator(); + } + CacheResolver operationCacheResolver; + if (StringUtils.hasText(operation.getCacheResolver())) { + operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class); + } + else if (StringUtils.hasText(operation.getCacheManager())) { + CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class); + operationCacheResolver = new SimpleCacheResolver(cacheManager); + } + else { + operationCacheResolver = getCacheResolver(); + } + metadata = new CacheOperationMetadata(operation, method, targetClass, + operationKeyGenerator, operationCacheResolver); + metadataCache.put(cacheKey, metadata); + } + return metadata; + } + + /** + * Return a bean with the specified name and type. Used to resolve services that + * are referenced by name in a {@link CacheOperation}. + * + * @param beanName the name of the bean, as defined by the operation + * @param expectedType type type for the bean + * @return the bean matching that name + * @throws org.springframework.beans.factory.NoSuchBeanDefinitionException if such bean does not exist + * @see CacheOperation#keyGenerator + * @see CacheOperation#cacheManager + * @see CacheOperation#cacheResolver + */ + protected T getBean(String beanName, Class expectedType) { + return BeanFactoryAnnotationUtils.qualifiedBeanOfType( + applicationContext, expectedType, beanName); + } + + /** + * Clear the cached metadata. + */ + protected void clearMetadataCache() { + metadataCache.clear(); } protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { @@ -245,7 +331,7 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio private void processCacheEvicts(Collection contexts, boolean beforeInvocation, Object result) { for (CacheOperationContext context : contexts) { - CacheEvictOperation operation = (CacheEvictOperation) context.operation; + CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation; if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) { performCacheEvict(context, operation, result); } @@ -272,7 +358,7 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio private void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, Object key) { if (logger.isTraceEnabled()) { logger.trace("Invalidating " + (key != null ? "cache key [" + key + "]" : "entire cache") + - " for operation " + operation + " on method " + context.method); + " for operation " + operation + " on method " + context.metadata.method); } } @@ -327,7 +413,8 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio private boolean isConditionPassing(CacheOperationContext context, Object result) { boolean passing = context.isConditionPassing(result); if (!passing && logger.isTraceEnabled()) { - logger.trace("Cache condition failed on method " + context.method + " for operation " + context.operation); + logger.trace("Cache condition failed on method " + context.metadata.method + + " for operation " + context.metadata.operation); } return passing; } @@ -335,9 +422,9 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio private Object generateKey(CacheOperationContext context, Object result) { Object key = context.generateKey(result); Assert.notNull(key, "Null key returned for cache operation (maybe you are using named params " + - "on classes without debug info?) " + context.operation); + "on classes without debug info?) " + context.metadata.operation); if (logger.isTraceEnabled()) { - logger.trace("Computed cache key " + key + " for operation " + context.operation); + logger.trace("Computed cache key " + key + " for operation " + context.metadata.operation); } return key; } @@ -349,7 +436,7 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio new LinkedMultiValueMap, CacheOperationContext>(); public CacheOperationContexts(Collection operations, - Method method, Object[] args, Object target, Class targetClass) { + Method method, Object[] args, Object target, Class targetClass) { for (CacheOperation operation : operations) { this.contexts.add(operation.getClass(), getOperationContext(operation, method, args, target, targetClass)); @@ -358,51 +445,70 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio public Collection get(Class operationClass) { Collection result = this.contexts.get(operationClass); - return (result != null ? result : Collections. emptyList()); + return (result != null ? result : Collections.emptyList()); } } - protected class CacheOperationContext { + /** + * Metadata of a cache operation that does not depend on a particular invocation + * which makes it a good candidate for caching. + */ + protected static class CacheOperationMetadata { private final CacheOperation operation; - private final Method method; + private final Class targetClass; + private final KeyGenerator keyGenerator; + private final CacheResolver cacheResolver; + + public CacheOperationMetadata(CacheOperation operation, Method method, + Class targetClass, KeyGenerator keyGenerator, + CacheResolver cacheResolver) { + this.operation = operation; + this.method = method; + this.targetClass = targetClass; + this.keyGenerator = keyGenerator; + this.cacheResolver = cacheResolver; + } + } + + protected class CacheOperationContext implements CacheOperationInvocationContext { + + private final CacheOperationMetadata metadata; private final Object[] args; private final Object target; - private final Class targetClass; - private final Collection caches; - private final KeyGenerator operationKeyGenerator; - - private final CacheManager operationCacheManager; - - public CacheOperationContext(CacheOperation operation, Method method, - Object[] args, Object target, Class targetClass) { - this.operation = operation; - this.method = method; - this.args = extractArgs(method, args); + public CacheOperationContext(CacheOperationMetadata metadata, + Object[] args, Object target) { + this.metadata = metadata; + this.args = extractArgs(metadata.method, args); this.target = target; - this.targetClass = targetClass; - if (StringUtils.hasText(operation.getKeyGenerator())) { // TODO: exception mgt? - this.operationKeyGenerator = BeanFactoryAnnotationUtils.qualifiedBeanOfType( - applicationContext, KeyGenerator.class, operation.getKeyGenerator()); - } - else { - this.operationKeyGenerator = keyGenerator; - } - if (StringUtils.hasText(operation.getCacheManager())) { - this.operationCacheManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType( - applicationContext, CacheManager.class, operation.getCacheManager()); - } - else { - this.operationCacheManager = cacheManager; - } - this.caches = CacheAspectSupport.this.getCaches(operation, operationCacheManager); + this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver); + } + + @Override + public CacheOperation getOperation() { + return metadata.operation; + } + + @Override + public Object getTarget() { + return target; + } + + @Override + public Method getMethod() { + return metadata.method; + } + + @Override + public Object[] getArgs() { + return args; } private Object[] extractArgs(Method method, Object[] args) { @@ -417,24 +523,24 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio } protected boolean isConditionPassing(Object result) { - if (StringUtils.hasText(this.operation.getCondition())) { + if (StringUtils.hasText(this.metadata.operation.getCondition())) { EvaluationContext evaluationContext = createEvaluationContext(result); - return evaluator.condition(this.operation.getCondition(), this.method, evaluationContext); + return evaluator.condition(this.metadata.operation.getCondition(), this.metadata.method, evaluationContext); } return true; } protected boolean canPutToCache(Object value) { String unless = ""; - if (this.operation instanceof CacheableOperation) { - unless = ((CacheableOperation) this.operation).getUnless(); + if (this.metadata.operation instanceof CacheableOperation) { + unless = ((CacheableOperation) this.metadata.operation).getUnless(); } - else if (this.operation instanceof CachePutOperation) { - unless = ((CachePutOperation) this.operation).getUnless(); + else if (this.metadata.operation instanceof CachePutOperation) { + unless = ((CachePutOperation) this.metadata.operation).getUnless(); } if (StringUtils.hasText(unless)) { EvaluationContext evaluationContext = createEvaluationContext(value); - return !evaluator.unless(unless, this.method, evaluationContext); + return !evaluator.unless(unless, this.metadata.method, evaluationContext); } return true; } @@ -444,16 +550,16 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio * @return generated key (null if none can be generated) */ protected Object generateKey(Object result) { - if (StringUtils.hasText(this.operation.getKey())) { + if (StringUtils.hasText(this.metadata.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(result); - return evaluator.key(this.operation.getKey(), this.method, evaluationContext); + return evaluator.key(this.metadata.operation.getKey(), this.metadata.method, evaluationContext); } - return operationKeyGenerator.generate(this.target, this.method, this.args); + return metadata.keyGenerator.generate(this.target, this.metadata.method, this.args); } private EvaluationContext createEvaluationContext(Object result) { return evaluator.createEvaluationContext( - this.caches, this.method, this.args, this.target, this.targetClass, result); + this.caches, this.metadata.method, this.args, this.target, this.metadata.targetClass, result); } protected Collection getCaches() { @@ -482,4 +588,31 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio } } + private static class CacheOperationCacheKey { + + private final CacheOperation cacheOperation; + private final MethodCacheKey methodCacheKey; + + private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class targetClass) { + this.cacheOperation = cacheOperation; + this.methodCacheKey = new MethodCacheKey(method, targetClass); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CacheOperationCacheKey that = (CacheOperationCacheKey) o; + return cacheOperation.equals(that.cacheOperation) + && methodCacheKey.equals(that.methodCacheKey); + } + + @Override + public int hashCode() { + int result = cacheOperation.hashCode(); + result = 31 * result + methodCacheKey.hashCode(); + return result; + } + } } 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 2e1787ec87b..5013eb84560 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 @@ -28,7 +28,7 @@ import org.springframework.util.Assert; * @author Costin Leau * @author Stephane Nicoll */ -public abstract class CacheOperation { +public abstract class CacheOperation implements BasicCacheOperation { private Set cacheNames = Collections.emptySet(); @@ -40,9 +40,12 @@ public abstract class CacheOperation { private String cacheManager = ""; + private String cacheResolver = ""; + private String name = ""; + @Override public Set getCacheNames() { return cacheNames; } @@ -63,6 +66,10 @@ public abstract class CacheOperation { return cacheManager; } + public String getCacheResolver() { + return cacheResolver; + } + public String getName() { return name; } @@ -100,6 +107,11 @@ public abstract class CacheOperation { this.cacheManager = cacheManager; } + public void setCacheResolver(String cacheResolver) { + Assert.notNull(cacheManager); + this.cacheResolver = cacheResolver; + } + public void setName(String name) { Assert.hasText(name); this.name = name; @@ -151,6 +163,8 @@ public abstract class CacheOperation { result.append(this.keyGenerator); result.append("' | cacheManager='"); result.append(this.cacheManager); + result.append("' | cacheResolver='"); + result.append(this.cacheResolver); result.append("' | condition='"); result.append(this.condition); result.append("'"); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java index e1076a2a9f3..9de16712115 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java @@ -1,3 +1,19 @@ +/* + * 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.interceptor; import java.lang.reflect.Method; @@ -21,7 +37,6 @@ public final class MethodCacheKey { public MethodCacheKey(Method method, Class targetClass) { Assert.notNull(method, "method must be set."); - Assert.notNull(targetClass, "targetClass must be set."); this.method = method; this.targetClass = targetClass; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java new file mode 100644 index 00000000000..8bdf94a7724 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java @@ -0,0 +1,56 @@ +/* + * 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.interceptor; + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.cache.CacheManager; + +/** + * A {@link CacheResolver} that forces the resolution to a configurable + * collection of name(s) against a given {@link CacheManager}. + * + * @author Stephane Nicoll + */ +public class NamedCacheResolver extends BaseCacheResolver { + + private Collection cacheNames; + + public NamedCacheResolver(CacheManager cacheManager, String... cacheNames) { + super(cacheManager); + this.cacheNames = new ArrayList(Arrays.asList(cacheNames)); + } + + public NamedCacheResolver() { + } + + /** + * Set the cache name(s) that this resolver should use. + */ + public void setCacheNames(Collection cacheNames) { + this.cacheNames = cacheNames; + } + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return cacheNames; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java index a33f457d284..353de00c6f0 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java @@ -16,39 +16,29 @@ package org.springframework.cache.interceptor; -import java.util.ArrayList; import java.util.Collection; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.util.Assert; /** * A simple {@link CacheResolver} that resolves the {@link Cache} instance(s) * based on a configurable {@link CacheManager} and the name of the - * cache(s): {@link BasicCacheOperation#getCacheNames()} + * cache(s) as provided by {@link BasicCacheOperation#getCacheNames() getCacheNames()} * * @author Stephane Nicoll * @since 4.1 * @see BasicCacheOperation#getCacheNames() */ -public class SimpleCacheResolver implements CacheResolver { - - private final CacheManager cacheManager; +public class SimpleCacheResolver extends BaseCacheResolver { public SimpleCacheResolver(CacheManager cacheManager) { - this.cacheManager = cacheManager; + super(cacheManager); } @Override - public Collection resolveCaches(CacheOperationInvocationContext context) { - Collection result = new ArrayList(); - for (String cacheName : context.getOperation().getCacheNames()) { - Cache cache = cacheManager.getCache(cacheName); - Assert.notNull(cache, "Cannot find cache named '" + cacheName + "' for " + context.getOperation()); - result.add(cache); - } - return result; + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return context.getOperation().getCacheNames(); } } diff --git a/spring-context/src/test/java/org/springframework/cache/CacheTestUtils.java b/spring-context/src/test/java/org/springframework/cache/CacheTestUtils.java new file mode 100644 index 00000000000..d50462d5ec9 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/CacheTestUtils.java @@ -0,0 +1,47 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.support.SimpleCacheManager; + +/** + * General cache-related test utilities. + * + * @author Stephane Nicoll + */ +public class CacheTestUtils { + + /** + * Create a {@link SimpleCacheManager} with the specified cache(s). + * @param cacheNames the names of the caches to create + */ + public static CacheManager createSimpleCacheManager(String... cacheNames) { + SimpleCacheManager result = new SimpleCacheManager(); + List caches = new ArrayList(); + for (String cacheName : cacheNames) { + caches.add(new ConcurrentMapCache(cacheName)); + } + result.setCaches(caches); + result.afterPropertiesSet(); + return result; + } + +} 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 f8ae3ebb46a..89cf403dcc3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 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. @@ -138,24 +138,55 @@ public class AnnotationCacheOperationSourceTests { } @Test - public void fullClassLevelWithCustomKeyManager() { - Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); + public void testCustomCacheResolver() { + Collection ops = getOps(AnnotatedClass.class, "customCacheResolver", 1); CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classCacheManager", "custom", "classCacheName"); + assertEquals("Custom cache resolver not set", "custom", cacheOperation.getCacheResolver()); } @Test - public void fullClassLevelWithCustomCacheManager() { - Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); + public void testCustomCacheResolverInherited() { + Collection ops = getOps(AnnotatedClass.class, "customCacheResolverInherited", 1); CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "custom", "classKeyGenerator", "classCacheName"); + assertEquals("Custom cache resolver not set", "custom", cacheOperation.getCacheResolver()); + } + + @Test + public void testCacheResolverAndCacheManagerCannotBeSetTogether() { + try { + getOps(AnnotatedClass.class, "invalidCacheResolverAndCacheManagerSet"); + fail("Should have failed to parse @Cacheable annotation"); + } catch (IllegalStateException e) { + // expected + } } @Test public void fullClassLevelWithCustomCacheName() { Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheName", 1); CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classCacheManager", "classKeyGenerator", "custom"); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); + } + + @Test + public void fullClassLevelWithCustomKeyManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelKeyGenerator", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "custom", "", "classCacheResolver" , "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheManager() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + } + + @Test + public void fullClassLevelWithCustomCacheResolver() { + Collection ops = getOps(AnnotatedClassWithFullDefault.class, "methodLevelCacheResolver", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom" , "classCacheName"); } @Test @@ -168,20 +199,42 @@ public class AnnotationCacheOperationSourceTests { public void customClassLevelWithCustomCacheName() { Collection ops = getOps(AnnotatedClassWithCustomDefault.class, "methodLevelCacheName", 1); CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "classCacheManager", "classKeyGenerator", "custom"); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "classCacheResolver", "custom"); } @Test public void severalCacheConfigUseClosest() { Collection ops = getOps(MultipleCacheConfig.class, "multipleCacheConfig"); CacheOperation cacheOperation = ops.iterator().next(); - assertSharedConfig(cacheOperation, "", "", "myCache"); + assertSharedConfig(cacheOperation, "", "", "", "myCache"); } - private void assertSharedConfig(CacheOperation actual, String cacheManager, - String keyGenerator, String... cacheNames) { - assertEquals("Wrong cache manager", cacheManager, actual.getCacheManager()); + @Test + public void partialClassLevelWithCustomCacheManager() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheManager", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "custom", "", "classCacheName"); + } + + @Test + public void partialClassLevelWithCustomCacheResolver() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "methodLevelCacheResolver", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "", "custom", "classCacheName"); + } + + @Test + public void partialClassLevelWithNoCustomization() { + Collection ops = getOps(AnnotatedClassWithSomeDefault.class, "noCustomization", 1); + CacheOperation cacheOperation = ops.iterator().next(); + assertSharedConfig(cacheOperation, "classKeyGenerator", "classCacheManager", "", "classCacheName"); + } + + 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()); for (String cacheName : cacheNames) { assertTrue("Cache '"+cacheName+"' not found (got "+actual.getCacheNames(), actual.getCacheNames().contains(cacheName)); @@ -211,6 +264,10 @@ public class AnnotationCacheOperationSourceTests { public void customCacheManager() { } + @Cacheable(value = "test", cacheResolver = "custom") + public void customCacheResolver() { + } + @EvictFoo public void singleStereotype() { } @@ -237,15 +294,28 @@ public class AnnotationCacheOperationSourceTests { public void customCacheManagerInherited() { } + @CacheableFooCustomCacheResolver + public void customCacheResolverInherited() { + } + + @Cacheable(value = "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", - cacheManager = "classCacheManager", keyGenerator = "classKeyGenerator") + keyGenerator = "classKeyGenerator", + cacheManager = "classCacheManager", cacheResolver = "classCacheResolver") private static class AnnotatedClassWithFullDefault { + @Cacheable("custom") + public void methodLevelCacheName() { + } + @Cacheable(keyGenerator = "custom") public void methodLevelKeyGenerator() { } @@ -254,8 +324,8 @@ public class AnnotationCacheOperationSourceTests { public void methodLevelCacheManager() { } - @Cacheable("custom") - public void methodLevelCacheName() { + @Cacheable(cacheResolver = "custom") + public void methodLevelCacheResolver() { } } @@ -267,6 +337,25 @@ public class AnnotationCacheOperationSourceTests { } } + @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 { @@ -292,7 +381,12 @@ public class AnnotationCacheOperationSourceTests { @Target(ElementType.METHOD) @Cacheable(value = "foo", cacheManager = "custom") public @interface CacheableFooCustomCacheManager { + } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + @Cacheable(value = "foo", cacheResolver = "custom") + public @interface CacheableFooCustomCacheResolver { } @Retention(RetentionPolicy.RUNTIME) @@ -309,7 +403,8 @@ public class AnnotationCacheOperationSourceTests { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) - @CacheConfig(cacheManager = "classCacheManager", keyGenerator = "classKeyGenerator") + @CacheConfig(keyGenerator = "classKeyGenerator", + cacheManager = "classCacheManager", cacheResolver = "classCacheResolver") public @interface CacheConfigFoo { } } \ No newline at end of file diff --git a/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java b/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java index 8a5450de836..92ec015de71 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AbstractAnnotationTests.java @@ -22,13 +22,14 @@ import static org.junit.Assert.*; import java.util.Collection; import java.util.UUID; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; /** * Abstract annotation test (containing several reusable methods). @@ -40,7 +41,7 @@ import org.springframework.context.ApplicationContext; */ public abstract class AbstractAnnotationTests { - protected ApplicationContext ctx; + protected ConfigurableApplicationContext ctx; protected CacheableService cs; @@ -51,7 +52,7 @@ public abstract class AbstractAnnotationTests { protected CacheManager customCm; /** @return a refreshed application context */ - protected abstract ApplicationContext getApplicationContext(); + protected abstract ConfigurableApplicationContext getApplicationContext(); @Before public void setup() { @@ -67,6 +68,11 @@ public abstract class AbstractAnnotationTests { assertTrue(cn.contains("primary")); } + @After + public void tearDown() { + ctx.close(); + } + public void testCacheable(CacheableService service) throws Exception { Object o1 = new Object(); diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java index b6785792a58..44c2ae593e5 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationNamespaceDrivenTests.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. @@ -20,7 +20,7 @@ import static org.junit.Assert.assertSame; import org.junit.Test; import org.springframework.cache.interceptor.CacheInterceptor; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; /** @@ -30,7 +30,7 @@ import org.springframework.context.support.GenericXmlApplicationContext; public class AnnotationNamespaceDrivenTests extends AbstractAnnotationTests { @Override - protected ApplicationContext getApplicationContext() { + protected ConfigurableApplicationContext getApplicationContext() { return new GenericXmlApplicationContext( "/org/springframework/cache/config/annotationDrivenCacheNamespace.xml"); } diff --git a/spring-context/src/test/java/org/springframework/cache/config/AnnotationTests.java b/spring-context/src/test/java/org/springframework/cache/config/AnnotationTests.java index 04c07fd2083..8514c3ded2f 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/AnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/AnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 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.config; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; /** @@ -26,8 +26,9 @@ import org.springframework.context.support.GenericXmlApplicationContext; public class AnnotationTests extends AbstractAnnotationTests { @Override - protected ApplicationContext getApplicationContext() { + protected ConfigurableApplicationContext getApplicationContext() { return new GenericXmlApplicationContext( "/org/springframework/cache/config/annotationDrivenCacheConfig.xml"); } + } \ No newline at end of file diff --git a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java index e91a721b0c1..086f8fd1e03 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/CacheAdviceNamespaceTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 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. @@ -19,7 +19,7 @@ package org.springframework.cache.config; import org.junit.Assert; import org.junit.Test; import org.springframework.cache.interceptor.CacheInterceptor; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; /** @@ -29,7 +29,7 @@ import org.springframework.context.support.GenericXmlApplicationContext; public class CacheAdviceNamespaceTests extends AbstractAnnotationTests { @Override - protected ApplicationContext getApplicationContext() { + protected ConfigurableApplicationContext getApplicationContext() { return new GenericXmlApplicationContext( "/org/springframework/cache/config/cache-advice.xml"); } @@ -39,4 +39,5 @@ public class CacheAdviceNamespaceTests extends AbstractAnnotationTests { CacheInterceptor bean = ctx.getBean("cacheAdviceClass", CacheInterceptor.class); Assert.assertSame(ctx.getBean("keyGenerator"), bean.getKeyGenerator()); } + } diff --git a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index 73a11517017..cc0308a0b5b 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -16,22 +16,21 @@ package org.springframework.cache.config; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; - -import java.util.Arrays; +import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.BeanCreationException; import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.CachingConfigurer; +import org.springframework.cache.CacheTestUtils; +import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; -import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.interceptor.SimpleCacheResolver; import org.springframework.cache.support.NoOpCacheManager; -import org.springframework.cache.support.SimpleCacheManager; -import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -46,7 +45,7 @@ public class EnableCachingTests extends AbstractAnnotationTests { /** hook into superclass suite of tests */ @Override - protected ApplicationContext getApplicationContext() { + protected ConfigurableApplicationContext getApplicationContext() { return new AnnotationConfigApplicationContext(EnableCachingConfig.class); } @@ -111,19 +110,38 @@ public class EnableCachingTests extends AbstractAnnotationTests { } } + @Test + public void emptyConfigSupport() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(EmptyConfigSupportConfig.class); + + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertNotNull(ci.getCacheResolver()); + assertEquals(SimpleCacheResolver.class, ci.getCacheResolver().getClass()); + assertSame(context.getBean(CacheManager.class), + ((SimpleCacheResolver)ci.getCacheResolver()).getCacheManager()); + context.close(); + } + + @Test + public void bothSetOnlyResolverIsUsed() { + ConfigurableApplicationContext context = + new AnnotationConfigApplicationContext(FullCachingConfig.class); + + CacheInterceptor ci = context.getBean(CacheInterceptor.class); + assertSame(context.getBean("cacheResolver"), ci.getCacheResolver()); + assertSame(context.getBean("keyGenerator"), ci.getKeyGenerator()); + context.close(); + } + @Configuration @EnableCaching - static class EnableCachingConfig implements CachingConfigurer { + static class EnableCachingConfig extends CachingConfigurerSupport { @Override @Bean public CacheManager cacheManager() { - SimpleCacheManager cm = new SimpleCacheManager(); - cm.setCaches(Arrays.asList( - new ConcurrentMapCache("default"), - new ConcurrentMapCache("primary"), - new ConcurrentMapCache("secondary"))); - return cm; + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); } @Bean @@ -149,9 +167,7 @@ public class EnableCachingTests extends AbstractAnnotationTests { @Bean public CacheManager customCacheManager() { - SimpleCacheManager cm = new SimpleCacheManager(); - cm.setCaches(Arrays.asList(new ConcurrentMapCache("default"))); - return cm; + return CacheTestUtils.createSimpleCacheManager("default"); } } @@ -182,7 +198,7 @@ public class EnableCachingTests extends AbstractAnnotationTests { @Configuration @EnableCaching - static class MultiCacheManagerConfigurer implements CachingConfigurer { + static class MultiCacheManagerConfigurer extends CachingConfigurerSupport { @Bean public CacheManager cm1() { return new NoOpCacheManager(); } @Bean @@ -197,4 +213,37 @@ public class EnableCachingTests extends AbstractAnnotationTests { return null; } } + + @Configuration + @EnableCaching + static class EmptyConfigSupportConfig extends CachingConfigurerSupport { + + @Bean + public CacheManager cm() { + return new NoOpCacheManager(); + } + } + + @Configuration + @EnableCaching + static class FullCachingConfig extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SomeKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new NamedCacheResolver(cacheManager(), "foo"); + } + } } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomisationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomisationTests.java new file mode 100644 index 00000000000..a74bce760ba --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomisationTests.java @@ -0,0 +1,290 @@ +/* + * 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.interceptor; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.CacheTestUtils; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ReflectionUtils; + +/** + * Provides various {@link CacheResolver} customisations scenario + * + * @author Stephane Nicoll + */ +public class CacheResolverCustomisationTests { + + private CacheManager cacheManager; + + private CacheManager anotherCacheManager; + + private SimpleService simpleService; + + + @Before + public void setUp() { + ApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cacheManager = context.getBean("cacheManager", CacheManager.class); + this.anotherCacheManager = context.getBean("anotherCacheManager", CacheManager.class); + + this.simpleService = context.getBean(SimpleService.class); + } + + @Test + public void noCustomization() { + Cache cache = cacheManager.getCache("default"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = simpleService.getSimple(key); + assertCacheHit(key, value, cache); + } + + @Test + public void customCacheResolver() { + Cache cache = cacheManager.getCache("primary"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = simpleService.getWithCustomCacheResolver(key); + assertCacheHit(key, value, cache); + } + + @Test + public void customCacheManager() { + Cache cache = anotherCacheManager.getCache("default"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = simpleService.getWithCustomCacheManager(key); + assertCacheHit(key, value, cache); + } + + @Test + public void runtimeResolution() { + Cache defaultCache = cacheManager.getCache("default"); + Cache primaryCache = cacheManager.getCache("primary"); + + Object key = new Object(); + assertCacheMiss(key, defaultCache, primaryCache); + Object value = simpleService.getWithRuntimeCacheResolution(key, "default"); + assertCacheHit(key, value, defaultCache); + assertCacheMiss(key, primaryCache); + + Object key2 = new Object(); + assertCacheMiss(key2, defaultCache, primaryCache); + Object value2 = simpleService.getWithRuntimeCacheResolution(key2, "primary"); + assertCacheHit(key2, value2, primaryCache); + assertCacheMiss(key2, defaultCache); + } + + @Test + public void namedResolution() { + Cache cache = cacheManager.getCache("secondary"); + + Object key = new Object(); + assertCacheMiss(key, cache); + + Object value = simpleService.getWithNamedCacheResolution(key); + assertCacheHit(key, value, cache); + } + + @Test + public void noCacheResolved() { + Method m = ReflectionUtils.findMethod(SimpleService.class, "noCacheResolved", Object.class); + try { + simpleService.noCacheResolved(new Object()); + fail("Should have failed, no cache resolved"); + } catch (IllegalStateException e) { + String msg = e.getMessage(); + assertTrue("Reference to the method must be contained in the message", msg.contains(m.toString())); + } + } + + @Test + public void unknownCacheResolver() { + try { + simpleService.unknownCacheResolver(new Object()); + fail("Should have failed, no cache resolver with that name"); + } catch (NoSuchBeanDefinitionException e) { + assertEquals("Wrong bean name in exception", "unknownCacheResolver", e.getBeanName()); + } + } + + protected void assertCacheMiss(Object key, Cache... caches) { + for (Cache cache : caches) { + assertNull("No entry in " + cache + " should have been found with key " + key, cache.get(key)); + } + } + + protected void assertCacheHit(Object key, Object value, Cache... caches) { + for (Cache cache : caches) { + Cache.ValueWrapper wrapper = cache.get(key); + assertNotNull("An entry in " + cache + " should have been found with key " + key, wrapper); + assertEquals("Wrong value in " + cache + " for entry with key " + key, value, wrapper.get()); + } + } + + + @Configuration + @EnableCaching + static class Config extends CachingConfigurerSupport { + + @Override + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return null; + } + + @Bean + public CacheManager anotherCacheManager() { + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); + } + + @Bean + public CacheResolver primaryCacheResolver() { + return new NamedCacheResolver(cacheManager(), "primary"); + } + + @Bean + public CacheResolver secondaryCacheResolver() { + return new NamedCacheResolver(cacheManager(), "primary"); + } + + @Bean + public CacheResolver runtimeCacheResolver() { + return new RuntimeCacheResolver(cacheManager()); + } + + @Bean + public CacheResolver namedCacheResolver() { + NamedCacheResolver resolver = new NamedCacheResolver(); + resolver.setCacheManager(cacheManager()); + resolver.setCacheNames(Collections.singleton("secondary")); + return resolver; + } + + @Bean + public CacheResolver nullCacheResolver() { + return new NullCacheResolver(cacheManager()); + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + } + + @CacheConfig(cacheNames = "default") + static class SimpleService { + + private final AtomicLong counter = new AtomicLong(); + + @Cacheable + public Object getSimple(Object key) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "primaryCacheResolver") + public Object getWithCustomCacheResolver(Object key) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheManager = "anotherCacheManager") + public Object getWithCustomCacheManager(Object key) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "runtimeCacheResolver", key = "#p0") + public Object getWithRuntimeCacheResolution(Object key, String cacheName) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "namedCacheResolver") + public Object getWithNamedCacheResolution(Object key) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "nullCacheResolver") // No cache resolved for the operation + public Object noCacheResolved(Object key) { + return counter.getAndIncrement(); + } + + @Cacheable(cacheResolver = "unknownCacheResolver") // No such bean defined + public Object unknownCacheResolver(Object key) { + return counter.getAndIncrement(); + } + } + + /** + * Example of {@link CacheResolver} that resolve the caches at + * runtime (i.e. based on method invocation parameters). + *

Expects the second argument to hold the name of the cache to use + */ + private static class RuntimeCacheResolver extends BaseCacheResolver { + + private RuntimeCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + String cacheName = (String) context.getArgs()[1]; + return Collections.singleton(cacheName); + } + } + + private static class NullCacheResolver extends BaseCacheResolver { + + private NullCacheResolver(CacheManager cacheManager) { + super(cacheManager); + } + + @Override + protected Collection getCacheNames(CacheOperationInvocationContext context) { + return null; + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/MethodCacheKeyTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/MethodCacheKeyTests.java new file mode 100644 index 00000000000..19de44e84dc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/MethodCacheKeyTests.java @@ -0,0 +1,75 @@ +/* + * 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.interceptor; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.springframework.util.ReflectionUtils; + +/** + * @author Stephane Nicoll + */ +public class MethodCacheKeyTests { + + @Rule + public final TestName name = new TestName(); + + @Test + public void sameInstanceEquals() { + Method m = ReflectionUtils.findMethod(getClass(), name.getMethodName()); + MethodCacheKey instance = new MethodCacheKey(m, getClass()); + assertKeyEquals(instance, instance); + } + + @Test + public void equals() { + Method m = ReflectionUtils.findMethod(getClass(), name.getMethodName()); + MethodCacheKey first = new MethodCacheKey(m, getClass()); + MethodCacheKey second = new MethodCacheKey(m, getClass()); + + assertKeyEquals(first, second); + } + + @Test + public void equalsNoTarget() { + Method m = ReflectionUtils.findMethod(getClass(), name.getMethodName()); + MethodCacheKey first = new MethodCacheKey(m, null); + MethodCacheKey second = new MethodCacheKey(m, null); + + assertKeyEquals(first, second); + } + + @Test + public void noTargetClassNotEquals() { + Method m = ReflectionUtils.findMethod(getClass(), name.getMethodName()); + MethodCacheKey first = new MethodCacheKey(m, getClass()); + MethodCacheKey second = new MethodCacheKey(m, null); + + assertFalse(first.equals(second)); + } + + protected void assertKeyEquals(MethodCacheKey first, MethodCacheKey second) { + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } + +}