diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java index 71e6075cbd..646c7ece2a 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java @@ -55,6 +55,9 @@ public class ProxyJCacheConfiguration extends AbstractJCacheConfiguration { public JCacheInterceptor cacheInterceptor() { JCacheInterceptor interceptor = new JCacheInterceptor(); interceptor.setCacheOperationSource(cacheOperationSource()); + if (this.errorHandler != null) { + interceptor.setErrorHandler(this.errorHandler); + } return interceptor; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java index 089e5241d1..b162c84e14 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java @@ -24,6 +24,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.AbstractCacheInvoker; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.jcache.model.BaseCacheOperation; @@ -36,10 +38,14 @@ import org.springframework.cache.jcache.model.BaseCacheOperation; */ @SuppressWarnings("serial") public abstract class AbstractCacheInterceptor, A extends Annotation> - implements Serializable { + extends AbstractCacheInvoker implements Serializable { protected final Log logger = LogFactory.getLog(getClass()); + protected AbstractCacheInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + protected abstract Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) throws Throwable; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java index 11471d2d50..33259e31a0 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import javax.cache.annotation.CacheKeyInvocationContext; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.jcache.model.BaseKeyCacheOperation; @@ -34,6 +35,10 @@ import org.springframework.cache.jcache.model.BaseKeyCacheOperation; public abstract class AbstractKeyCacheInterceptor, A extends Annotation> extends AbstractCacheInterceptor { + protected AbstractKeyCacheInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + /** * Generate a key for the specified invocation. * @param context the context of the invocation diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java index 8a8432e507..7049661e1a 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java @@ -20,6 +20,7 @@ import javax.cache.annotation.CacheKeyInvocationContext; import javax.cache.annotation.CachePut; import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.jcache.model.CachePutOperation; @@ -33,6 +34,10 @@ import org.springframework.cache.jcache.model.CachePutOperation; @SuppressWarnings("serial") public class CachePutInterceptor extends AbstractKeyCacheInterceptor { + public CachePutInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + @Override protected Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { @@ -65,7 +70,7 @@ public class CachePutInterceptor extends AbstractKeyCacheInterceptor context, Object value) { Object key = generateKey(context); Cache cache = resolveCache(context); - cache.put(key, value); + doPut(cache, key, value); } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java index 2d215278a4..25b866932f 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java @@ -19,6 +19,7 @@ package org.springframework.cache.jcache.interceptor; import javax.cache.annotation.CacheRemoveAll; import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.jcache.model.CacheRemoveAllOperation; @@ -33,6 +34,10 @@ import org.springframework.cache.jcache.model.CacheRemoveAllOperation; public class CacheRemoveAllInterceptor extends AbstractCacheInterceptor { + protected CacheRemoveAllInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + @Override protected Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { @@ -66,7 +71,7 @@ public class CacheRemoveAllInterceptor logger.trace("Invalidating entire cache '" + cache.getName() + "' for operation " + context.getOperation()); } - cache.clear(); + doClear(cache); } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java index 2d0b7c3697..4fcb3fc7c2 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java @@ -19,6 +19,7 @@ package org.springframework.cache.jcache.interceptor; import javax.cache.annotation.CacheRemove; import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.jcache.model.CacheRemoveOperation; @@ -32,6 +33,10 @@ import org.springframework.cache.jcache.model.CacheRemoveOperation; @SuppressWarnings("serial") public class CacheRemoveEntryInterceptor extends AbstractKeyCacheInterceptor { + protected CacheRemoveEntryInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + @Override protected Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { @@ -66,7 +71,7 @@ public class CacheRemoveEntryInterceptor extends AbstractKeyCacheInterceptor { + public CacheResultInterceptor(CacheErrorHandler errorHandler) { + super(errorHandler); + } + @Override protected Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { @@ -46,7 +51,7 @@ public class CacheResultInterceptor extends AbstractKeyCacheInterceptor the type of the exception * @return a clone exception with a rewritten call stack composed of the current * call stack up to (included) the common ancestor specified by the {@code className} and * {@code methodName} arguments, followed by stack trace elements of the specified diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java index 4d5117b5aa..8a4202b687 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.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.jcache.interceptor; import java.lang.annotation.Annotation; @@ -8,6 +24,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.interceptor.AbstractCacheInvoker; import org.springframework.cache.interceptor.BasicCacheOperation; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; @@ -36,7 +53,7 @@ import org.springframework.util.Assert; * @see KeyGeneratorAdapter * @see CacheResolverAdapter */ -public class JCacheAspectSupport implements InitializingBean { +public class JCacheAspectSupport extends AbstractCacheInvoker implements InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); @@ -44,13 +61,13 @@ public class JCacheAspectSupport implements InitializingBean { private boolean initialized = false; - private final CacheResultInterceptor cacheResultInterceptor = new CacheResultInterceptor(); + private CacheResultInterceptor cacheResultInterceptor; - private final CachePutInterceptor cachePutInterceptor = new CachePutInterceptor(); + private CachePutInterceptor cachePutInterceptor; - private final CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(); + private CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor; - private final CacheRemoveAllInterceptor cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(); + private CacheRemoveAllInterceptor cacheRemoveAllInterceptor; public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { Assert.notNull(cacheOperationSource); @@ -67,6 +84,13 @@ public class JCacheAspectSupport implements InitializingBean { public void afterPropertiesSet() { Assert.state(this.cacheOperationSource != null, "The 'cacheOperationSource' property is required: " + "If there are no cacheable methods, then don't use a cache aspect."); + Assert.state(this.getErrorHandler() != null, "The 'errorHandler' is required."); + + this.cacheResultInterceptor = new CacheResultInterceptor(getErrorHandler()); + this.cachePutInterceptor = new CachePutInterceptor(getErrorHandler()); + this.cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(getErrorHandler()); + this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler()); + this.initialized = true; } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java index 3bb27f06cc..f81402e538 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java @@ -43,6 +43,8 @@ public abstract class AbstractJCacheAnnotationTests { @Rule public final TestName name = new TestName(); + protected ApplicationContext ctx; + private JCacheableService service; private CacheManager cacheManager; @@ -51,9 +53,9 @@ public abstract class AbstractJCacheAnnotationTests { @Before public void setUp() { - ApplicationContext context = getApplicationContext(); - service = context.getBean(JCacheableService.class); - cacheManager = context.getBean("cacheManager", CacheManager.class); + ctx = getApplicationContext(); + service = ctx.getBean(JCacheableService.class); + cacheManager = ctx.getBean("cacheManager", CacheManager.class); } @Test 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 8bf177b531..6952697b38 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 @@ -27,13 +27,16 @@ 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.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.NamedCacheResolver; +import org.springframework.cache.interceptor.SimpleCacheErrorHandler; 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.JCacheInterceptor; import org.springframework.cache.jcache.interceptor.SimpleExceptionCacheResolver; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.cache.support.SimpleCacheManager; @@ -63,6 +66,8 @@ public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { cos.getDefaultCacheResolver()); assertSame(context.getBean("exceptionCacheResolver", CacheResolver.class), cos.getDefaultExceptionCacheResolver()); + JCacheInterceptor interceptor = context.getBean(JCacheInterceptor.class); + assertSame(context.getBean("errorHandler", CacheErrorHandler.class), interceptor.getErrorHandler()); } @Test @@ -138,6 +143,12 @@ public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { return new SimpleKeyGenerator(); } + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + @Override @Bean public CacheResolver cacheResolver() { diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java index 3cff4bc9d3..7391c8661d 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java @@ -16,6 +16,12 @@ package org.springframework.cache.jcache.config; +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; import org.springframework.context.ApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; @@ -30,4 +36,10 @@ public class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { "/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml"); } + @Test + public void testCacheErrorHandler() { + JCacheInterceptor ci = ctx.getBean(JCacheInterceptor.class); + assertSame(ctx.getBean("errorHandler", CacheErrorHandler.class), ci.getErrorHandler()); + } + } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java new file mode 100644 index 0000000000..97978a3c16 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheErrorHandlerTests.java @@ -0,0 +1,181 @@ +/* + * 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.interceptor; + +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; +import javax.cache.annotation.CacheValue; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * + * @author Stephane Nicoll + */ +public class JCacheErrorHandlerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Cache cache; + + private CacheErrorHandler errorHandler; + + private SimpleService simpleService; + + @Before + public void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cache = context.getBean("mockCache", Cache.class); + this.errorHandler = context.getBean(CacheErrorHandler.class); + this.simpleService = context.getBean(SimpleService.class); + } + + @Test + public void getFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + SimpleGeneratedCacheKey key = new SimpleGeneratedCacheKey(0L); + doThrow(exception).when(cache).get(key); + + this.simpleService.get(0L); + verify(errorHandler).handleCacheGetError(exception, cache, key); + } + + @Test + public void putFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + SimpleGeneratedCacheKey key = new SimpleGeneratedCacheKey(0L); + doThrow(exception).when(cache).put(key, 234L); + + this.simpleService.put(0L, 234L); + verify(errorHandler).handleCachePutError(exception, cache, key, 234L); + } + + @Test + public void evictFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + SimpleGeneratedCacheKey key = new SimpleGeneratedCacheKey(0L); + doThrow(exception).when(cache).evict(key); + + this.simpleService.evict(0L); + verify(errorHandler).handleCacheEvictError(exception, cache, key); + } + + @Test + public void clearFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + doThrow(exception).when(cache).clear(); + + this.simpleService.clear(); + verify(errorHandler).handleCacheClearError(exception, cache); + } + + + @Configuration + @EnableCaching + static class Config { + + @Bean(name = "jCacheInterceptor") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheInterceptor cacheInterceptor() { + JCacheInterceptor interceptor = new JCacheInterceptor(); + interceptor.setCacheOperationSource(cacheOperationSource()); + interceptor.setErrorHandler(errorHandler()); + return interceptor; + } + + @Bean + public CacheErrorHandler errorHandler() { + return mock(CacheErrorHandler.class); + } + + @Bean(name = "jCacheOperationSource") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheOperationSource cacheOperationSource() { + DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + source.setCacheManager(cacheManager()); + return source; + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Arrays.asList(mockCache())); + return cacheManager; + } + + @Bean + public Cache mockCache() { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn("test"); + return cache; + } + + } + + @CacheDefaults(cacheName = "test") + public static class SimpleService { + private AtomicLong counter = new AtomicLong(); + + @CacheResult + public Object get(long id) { + return counter.getAndIncrement(); + } + + @CachePut + public void put(long id, @CacheValue Object object) { + } + + @CacheRemove + public void evict(long id) { + } + + @CacheRemoveAll + public void clear() { + } + } +} diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml index 76565f1e4e..bd2a2edb89 100644 --- a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml @@ -7,7 +7,7 @@ http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> - + @@ -26,6 +26,8 @@ + + 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 47efe817da..0449abf50d 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.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Configuration; @@ -51,6 +52,8 @@ public abstract class AbstractCachingConfiguration protected KeyGenerator keyGenerator; + protected CacheErrorHandler errorHandler; + @Autowired(required=false) private Collection cacheManagerBeans; @@ -115,6 +118,7 @@ public abstract class AbstractCachingConfiguration this.cacheManager = config.cacheManager(); this.cacheResolver = config.cacheResolver(); this.keyGenerator = config.keyGenerator(); + this.errorHandler = config.errorHandler(); } } 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 524e9ad901..0826f00236 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 @@ -17,6 +17,7 @@ package org.springframework.cache.annotation; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; @@ -104,4 +105,26 @@ public interface CachingConfigurer { */ KeyGenerator keyGenerator(); + /** + * Return the {@link CacheErrorHandler} to use to handle cache-related errors. + *

By default,{@link org.springframework.cache.interceptor.SimpleCacheErrorHandler} + * is used and simply throws the exception back at the client. + *

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

+	 * @Configuration
+	 * @EnableCaching
+	 * public class AppConfig extends CachingConfigurerSupport {
+	 *     @Bean // important!
+	 *     @Override
+	 *     public CacheErrorHandler errorHandler() {
+	 *         // configure and return CacheErrorHandler instance
+	 *     }
+	 *     // ...
+	 * }
+	 * 
+ * See @{@link EnableCaching} for more complete examples. + */ + CacheErrorHandler errorHandler(); + } 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 index 29801d6633..1c2a868cc9 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java @@ -17,6 +17,7 @@ package org.springframework.cache.annotation; import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; @@ -45,4 +46,8 @@ public class CachingConfigurerSupport implements CachingConfigurer { return null; } + @Override + public CacheErrorHandler errorHandler() { + return null; + } } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java index c475a3f8e4..5be2984942 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java @@ -91,11 +91,11 @@ import org.springframework.core.Ordered; *

For those that wish to establish a more direct relationship between * {@code @EnableCaching} and the exact cache manager bean to be used, * the {@link CachingConfigurer} callback interface may be implemented - notice the - * {@code implements} clause and the {@code @Override}-annotated methods below: + * the {@code @Override}-annotated methods below: *

  * @Configuration
  * @EnableCaching
- * public class AppConfig implements CachingConfigurer {
+ * public class AppConfig extends CachingConfigurerSupport {
  *     @Bean
  *     public MyService myService() {
  *         // configure and return a class having @Cacheable methods
@@ -128,9 +128,14 @@ import org.springframework.core.Ordered;
  * {@code @EnableCaching} will configure Spring's
  * {@link org.springframework.cache.interceptor.SimpleKeyGenerator SimpleKeyGenerator}
  * for this purpose, but when implementing {@code CachingConfigurer}, a key generator
- * must be provided explicitly. Return {@code new SimpleKeyGenerator()} from this method
- * if no customization is necessary. See {@link CachingConfigurer} Javadoc for further
- * details.
+ * must be provided explicitly. Return {@code null} or {@code new SimpleKeyGenerator()}
+ * from this method if no customization is necessary.
+ *
+ * 

{@link CachingConfigurer} offers additional customization options: it is recommended + * to extend from {@link org.springframework.cache.annotation.CachingConfigurerSupport + * CachingConfigurerSupport} that provides a default implementation for all methods which + * can be useful if you do not need to customize everything. See {@link CachingConfigurer} + * Javadoc for further details. * *

The {@link #mode()} attribute controls how advice is applied; if the mode is * {@link AdviceMode#PROXY} (the default), then the other attributes such as 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 aeb03c368e..b45707d66d 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 @@ -68,6 +68,9 @@ public class ProxyCachingConfiguration extends AbstractCachingConfigurationTypically, failing to retrieve an object from the cache with + * a given id can be transparently managed as a cache miss by not + * throwing back such exception. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface CacheErrorHandler { + + /** + * Handle the given runtime exception thrown by the cache provider when + * retrieving an item with the specified {@code key}, possibly + * rethrowing it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to get the item + * @see Cache#get(Object) + */ + void handleCacheGetError(RuntimeException exception, Cache cache, Object key); + + /** + * Handle the given runtime exception thrown by the cache provider when + * updating an item with the specified {@code key} and {@code value}, + * possibly rethrowing it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to update the item + * @param value the value to associate with the key + * @see Cache#put(Object, Object) + */ + void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value); + + /** + * Handle the given runtime exception thrown by the cache provider when + * clearing an item with the specified {@code key}, possibly rethrowing + * it as a fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache + * @param key the key used to clear the item + */ + void handleCacheEvictError(RuntimeException exception, Cache cache, Object key); + + /** + * Handle the given runtime exception thrown by the cache provider when + * clearing the specified {@link Cache}, possibly rethrowing it as a + * fatal exception. + * @param exception the exception thrown by the cache provider + * @param cache the cache to clear + */ + void handleCacheClearError(RuntimeException exception, Cache cache); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java new file mode 100644 index 0000000000..fde1f536bd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java @@ -0,0 +1,49 @@ +/* + * 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 org.springframework.cache.Cache; + +/** + * A simple {@link CacheErrorHandler} that does not handle the + * exception at all, simply throwing it back at the client. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class SimpleCacheErrorHandler implements CacheErrorHandler { + + @Override + public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) { + throw exception; + } + + @Override + public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) { + throw exception; + } + + @Override + public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) { + throw exception; + } + + @Override + public void handleCacheClearError(RuntimeException exception, Cache cache) { + throw exception; + } +} diff --git a/spring-context/src/main/resources/META-INF/spring.schemas b/spring-context/src/main/resources/META-INF/spring.schemas index b246784089..da5b581acf 100644 --- a/spring-context/src/main/resources/META-INF/spring.schemas +++ b/spring-context/src/main/resources/META-INF/spring.schemas @@ -27,4 +27,5 @@ http\://www.springframework.org/schema/task/spring-task.xsd=org/springframework/ http\://www.springframework.org/schema/cache/spring-cache-3.1.xsd=org/springframework/cache/config/spring-cache-3.1.xsd http\://www.springframework.org/schema/cache/spring-cache-3.2.xsd=org/springframework/cache/config/spring-cache-3.2.xsd http\://www.springframework.org/schema/cache/spring-cache-4.0.xsd=org/springframework/cache/config/spring-cache-4.0.xsd -http\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache-4.0.xsd +http\://www.springframework.org/schema/cache/spring-cache-4.1.xsd=org/springframework/cache/config/spring-cache-4.1.xsd +http\://www.springframework.org/schema/cache/spring-cache.xsd=org/springframework/cache/config/spring-cache-4.1.xsd diff --git a/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-4.1.xsd b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-4.1.xsd new file mode 100644 index 0000000000..9c6b547c9e --- /dev/null +++ b/spring-context/src/main/resources/org/springframework/cache/config/spring-cache-4.1.xsd @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 44c2ae593e..61b9f40c1a 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 @@ -16,9 +16,11 @@ package org.springframework.cache.config; -import static org.junit.Assert.assertSame; +import static org.junit.Assert.*; import org.junit.Test; + +import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheInterceptor; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; @@ -36,9 +38,16 @@ public class AnnotationNamespaceDrivenTests extends AbstractAnnotationTests { } @Test - public void testKeyStrategy() throws Exception { + public void testKeyStrategy() { CacheInterceptor ci = ctx.getBean("org.springframework.cache.interceptor.CacheInterceptor#0", CacheInterceptor.class); assertSame(ctx.getBean("keyGenerator"), ci.getKeyGenerator()); } + + @Test + public void testCacheErrorHandler() { + CacheInterceptor ci = ctx.getBean("org.springframework.cache.interceptor.CacheInterceptor#0", + CacheInterceptor.class); + assertSame(ctx.getBean("errorHandler", CacheErrorHandler.class), ci.getErrorHandler()); + } } 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 cc0308a0b5..4846deb007 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 @@ -24,10 +24,12 @@ import org.springframework.cache.CacheManager; import org.springframework.cache.CacheTestUtils; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheErrorHandler; 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.SimpleCacheErrorHandler; import org.springframework.cache.interceptor.SimpleCacheResolver; import org.springframework.cache.support.NoOpCacheManager; import org.springframework.context.ConfigurableApplicationContext; @@ -50,11 +52,17 @@ public class EnableCachingTests extends AbstractAnnotationTests { } @Test - public void testKeyStrategy() throws Exception { + public void testKeyStrategy() { CacheInterceptor ci = ctx.getBean(CacheInterceptor.class); assertSame(ctx.getBean("keyGenerator", KeyGenerator.class), ci.getKeyGenerator()); } + @Test + public void testCacheErrorHandler() { + CacheInterceptor ci = ctx.getBean(CacheInterceptor.class); + assertSame(ctx.getBean("errorHandler", CacheErrorHandler.class), ci.getErrorHandler()); + } + // --- local tests ------- @Test @@ -160,6 +168,12 @@ public class EnableCachingTests extends AbstractAnnotationTests { return new SomeKeyGenerator(); } + @Override + @Bean + public CacheErrorHandler errorHandler() { + return new SimpleCacheErrorHandler(); + } + @Bean public KeyGenerator customKeyGenerator() { return new SomeCustomKeyGenerator(); diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java new file mode 100644 index 0000000000..b69990d43b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheErrorHandlerTests.java @@ -0,0 +1,241 @@ +/* + * 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.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.doReturn; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.*; +import static org.mockito.BDDMockito.verify; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.AnnotationCacheOperationSource; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Stephane Nicoll + */ +public class CacheErrorHandlerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Cache cache; + + private CacheInterceptor cacheInterceptor; + + private CacheErrorHandler errorHandler; + + private SimpleService simpleService; + + @Before + public void setup() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + this.cache = context.getBean("mockCache", Cache.class); + this.cacheInterceptor = context.getBean(CacheInterceptor.class); + this.errorHandler = context.getBean(CacheErrorHandler.class); + this.simpleService = context.getBean(SimpleService.class); + } + + @Test + public void getFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + doThrow(exception).when(cache).get(0L); + + Object result = this.simpleService.get(0L); + verify(errorHandler).handleCacheGetError(exception, cache, 0L); + verify(cache).get(0L); + verify(cache).put(0L, result); // result of the invocation + } + + @Test + public void getAndPutFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + doThrow(exception).when(cache).get(0L); + doThrow(exception).when(cache).put(0L, 0L); // Update of the cache will fail as well + + Object counter = this.simpleService.get(0L); + + doReturn(new SimpleValueWrapper(2L)).when(cache).get(0L); + Object counter2 = this.simpleService.get(0L); + Object counter3 = this.simpleService.get(0L); + assertNotSame(counter, counter2); + assertEquals(counter2, counter3); + } + + @Test + public void getFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on get"); + doThrow(exception).when(cache).get(0L); + + cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + thrown.expect(is(exception)); + this.simpleService.get(0L); + } + + @Test + public void putFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + doThrow(exception).when(cache).put(0L, 0L); + + this.simpleService.put(0L); + verify(errorHandler).handleCachePutError(exception, cache, 0L, 0L); + } + + @Test + public void putFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on put"); + doThrow(exception).when(cache).put(0L, 0L); + + cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + thrown.expect(is(exception)); + this.simpleService.put(0L); + } + + @Test + public void evictFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + doThrow(exception).when(cache).evict(0L); + + this.simpleService.evict(0L); + verify(errorHandler).handleCacheEvictError(exception, cache, 0L); + } + + @Test + public void evictFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + doThrow(exception).when(cache).evict(0L); + + cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + thrown.expect(is(exception)); + this.simpleService.evict(0L); + } + + @Test + public void clearFail() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + doThrow(exception).when(cache).clear(); + + this.simpleService.clear(); + verify(errorHandler).handleCacheClearError(exception, cache); + } + + @Test + public void clearFailProperException() { + UnsupportedOperationException exception = new UnsupportedOperationException("Test exception on evict"); + doThrow(exception).when(cache).clear(); + + cacheInterceptor.setErrorHandler(new SimpleCacheErrorHandler()); + + thrown.expect(is(exception)); + this.simpleService.clear(); + } + + + @Configuration + @EnableCaching + static class Config { + + @Bean + public CacheInterceptor cacheInterceptor() { + CacheInterceptor cacheInterceptor = new CacheInterceptor(); + cacheInterceptor.setCacheManager(cacheManager()); + cacheInterceptor.setCacheOperationSources(cacheOperationSource()); + cacheInterceptor.setErrorHandler(errorHandler()); + return cacheInterceptor; + } + + @Bean + public CacheErrorHandler errorHandler() { + return mock(CacheErrorHandler.class); + } + + @Bean + public CacheOperationSource cacheOperationSource() { + return new AnnotationCacheOperationSource(); + + } + + @Bean + public SimpleService simpleService() { + return new SimpleService(); + } + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Arrays.asList(mockCache())); + return cacheManager; + } + + @Bean + public Cache mockCache() { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn("test"); + return cache; + } + + } + + @CacheConfig(cacheNames = "test") + public static class SimpleService { + private AtomicLong counter = new AtomicLong(); + + @Cacheable + public Object get(long id) { + return counter.getAndIncrement(); + } + + @CachePut + public Object put(long id) { + return counter.getAndIncrement(); + } + + @CacheEvict + public void evict(long id) { + } + + @CacheEvict(allEntries = true) + public void clear() { + } + } + +} diff --git a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml index be23c91e6b..3517dae80e 100644 --- a/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml +++ b/spring-context/src/test/resources/org/springframework/cache/config/annotationDrivenCacheNamespace.xml @@ -7,7 +7,8 @@ http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> - + @@ -31,6 +32,8 @@ + + diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 754cd5785b..d33faa527c 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -47196,6 +47196,17 @@ application through AOP. The configuration is intentionally similar with that of | Name of cache manager to use. Only required if the name of the cache manager is not `cacheManager`. +| `key-generator` +| N/A (See `CachingConfigurer` javadocs) +| `SimpleKeyGenerator` +| Name of the custom key generator to use. + +| `error-handler` +| N/A (See `CachingConfigurer` javadocs) +| `SimpleCacheErrorHandler` +| Name of the custom cache error handler to use. By default, any exception throw during + a cache related operations are thrown back at the client. + | `mode` | `mode` | proxy