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 8a4202b687..b6a4c881a3 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 @@ -89,7 +89,7 @@ public class JCacheAspectSupport extends AbstractCacheInvoker implements Initial this.cacheResultInterceptor = new CacheResultInterceptor(getErrorHandler()); this.cachePutInterceptor = new CachePutInterceptor(getErrorHandler()); this.cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(getErrorHandler()); - this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler()); + this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler()); this.initialized = true; } @@ -130,26 +130,55 @@ public class JCacheAspectSupport extends AbstractCacheInvoker implements Initial @SuppressWarnings("unchecked") private Object execute(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + + CacheOperationInvoker adapter = new CacheOperationInvokerAdapter(invoker); + BasicCacheOperation operation = context.getOperation(); if (operation instanceof CacheResultOperation) { return cacheResultInterceptor.invoke( - (CacheOperationInvocationContext) context, invoker); + (CacheOperationInvocationContext) context, adapter); } else if (operation instanceof CachePutOperation) { return cachePutInterceptor.invoke( - (CacheOperationInvocationContext) context, invoker); + (CacheOperationInvocationContext) context, adapter); } else if (operation instanceof CacheRemoveOperation) { return cacheRemoveEntryInterceptor.invoke( - (CacheOperationInvocationContext) context, invoker); + (CacheOperationInvocationContext) context, adapter); } else if (operation instanceof CacheRemoveAllOperation) { return cacheRemoveAllInterceptor.invoke( - (CacheOperationInvocationContext) context, invoker); + (CacheOperationInvocationContext) context, adapter); } else { throw new IllegalArgumentException("Could not handle " + operation); } } + /** + * Execute the underlying operation (typically in case of cache miss) and return + * the result of the invocation. If an exception occurs it will be wrapped in + * a {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled + * or modified but it must be wrapped in a + * {@link CacheOperationInvoker.ThrowableWrapper} as well. + * @param invoker the invoker handling the operation being cached + * @return the result of the invocation + * @see CacheOperationInvoker#invoke() + */ + protected Object invokeOperation(CacheOperationInvoker invoker) { + return invoker.invoke(); + } + + private class CacheOperationInvokerAdapter implements CacheOperationInvoker { + + private final CacheOperationInvoker delegate; + + private CacheOperationInvokerAdapter(CacheOperationInvoker delegate) {this.delegate = delegate;} + + @Override + public Object invoke() throws ThrowableWrapper { + return invokeOperation(delegate); + } + } + } diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java new file mode 100644 index 0000000000..a03f6e5ebf --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheCustomInterceptorTests.java @@ -0,0 +1,157 @@ +/* + * 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 static org.junit.Assert.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +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.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Stephane Nicoll + */ +public class JCacheCustomInterceptorTests { + + protected ConfigurableApplicationContext ctx; + + protected JCacheableService cs; + + protected Cache exceptionCache; + + @Before + public void setup() { + ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); + cs = ctx.getBean("service", JCacheableService.class); + exceptionCache = ctx.getBean("exceptionCache", Cache.class); + } + + @After + public void tearDown() { + ctx.close(); + } + + @Test + public void onlyOneInterceptorIsAvailable() { + Map interceptors = ctx.getBeansOfType(JCacheInterceptor.class); + assertEquals("Only one interceptor should be defined", 1, interceptors.size()); + JCacheInterceptor interceptor = interceptors.values().iterator().next(); + assertEquals("Custom interceptor not defined", TestCacheInterceptor.class, interceptor.getClass()); + } + + @Test + public void customInterceptorAppliesWithRuntimeException() { + Object o = cs.cacheWithException("id", true); + assertEquals(55L, o); // See TestCacheInterceptor + } + + @Test + public void customInterceptorAppliesWithCheckedException() { + try { + cs.cacheWithCheckedException("id", true); + fail("Should have failed"); + } + catch (RuntimeException e) { + assertNotNull("missing original exception", e.getCause()); + assertEquals(IOException.class, e.getCause().getClass()); + } + catch (Exception e) { + fail("Wrong exception type " + e); + } + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + SimpleCacheManager cm = new SimpleCacheManager(); + cm.setCaches(Arrays.asList( + defaultCache(), + exceptionCache())); + return cm; + } + + @Bean + public JCacheableService service() { + return new AnnotatedJCacheableService(defaultCache()); + } + + @Bean + public Cache defaultCache() { + return new ConcurrentMapCache("default"); + } + + @Bean + public Cache exceptionCache() { + return new ConcurrentMapCache("exception"); + } + + @Bean + public JCacheInterceptor jCacheInterceptor(JCacheOperationSource cacheOperationSource) { + JCacheInterceptor cacheInterceptor = new TestCacheInterceptor(); + cacheInterceptor.setCacheOperationSource(cacheOperationSource); + return cacheInterceptor; + } + } + + /** + * A test {@link org.springframework.cache.interceptor.CacheInterceptor} that handles special exception + * types. + */ + static class TestCacheInterceptor extends JCacheInterceptor { + + @Override + protected Object invokeOperation(CacheOperationInvoker invoker) { + try { + return super.invokeOperation(invoker); + } + catch (CacheOperationInvoker.ThrowableWrapper e) { + Throwable original = e.getOriginal(); + if (original.getClass() == UnsupportedOperationException.class) { + return 55L; + } + else { + throw new CacheOperationInvoker.ThrowableWrapper( + new RuntimeException("wrapping original", original)); + } + } + } + } + +} 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 518128e533..4d3e62f1a7 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 @@ -284,6 +284,20 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker return invoker.invoke(); } + /** + * Execute the underlying operation (typically in case of cache miss) and return + * the result of the invocation. If an exception occurs it will be wrapped in + * a {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled + * or modified but it must be wrapped in a + * {@link CacheOperationInvoker.ThrowableWrapper} as well. + * @param invoker the invoker handling the operation being cached + * @return the result of the invocation + * @see CacheOperationInvoker#invoke() + */ + protected Object invokeOperation(CacheOperationInvoker invoker) { + return invoker.invoke(); + } + private Class getTargetClass(Object target) { Class targetClass = AopProxyUtils.ultimateTargetClass(target); if (targetClass == null && target != null) { @@ -314,7 +328,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker // Invoke the method if don't have a cache hit if (result == null) { - result = new SimpleValueWrapper(invoker.invoke()); + result = new SimpleValueWrapper(invokeOperation(invoker)); } // Collect any explicit @CachePuts diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index 4ca3642c8d..199791e6a9 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -19,9 +19,10 @@ package org.springframework.cache.interceptor; /** * Abstract the invocation of a cache operation. * - *

Provide a special exception that can be used to indicate that the - * underlying invocation has thrown a checked exception, allowing the - * callers to threat these in a different manner if necessary. + *

Does not provide a way to transmit checked exceptions but + * provide a special exception that should be used to wrap any + * exception that was thrown by the underlying invocation. Callers + * are expected to handle this issue type specifically. * * @author Stephane Nicoll * @since 4.1 @@ -29,11 +30,11 @@ package org.springframework.cache.interceptor; public interface CacheOperationInvoker { /** - * Invoke the cache operation defined by this instance. Can throw a - * {@link ThrowableWrapper} if that operation wants to explicitly - * indicate that a checked exception has occurred. + * Invoke the cache operation defined by this instance. Wraps any + * exception that is thrown during the invocation in a + * {@link ThrowableWrapper}. * @return the result of the operation - * @throws ThrowableWrapper if a checked exception has been thrown + * @throws ThrowableWrapper if an error occurred while invoking the operation */ Object invoke() throws ThrowableWrapper; diff --git a/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java new file mode 100644 index 0000000000..653a1b2edd --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/config/CustomInterceptorTests.java @@ -0,0 +1,136 @@ +/* + * 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.config; + +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.CacheTestUtils; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheInterceptor; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheOperationSource; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * + * @author Stephane Nicoll + */ +public class CustomInterceptorTests { + + protected ConfigurableApplicationContext ctx; + + protected CacheableService cs; + + @Before + public void setup() { + ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class); + cs = ctx.getBean("service", CacheableService.class); + } + + @After + public void tearDown() { + ctx.close(); + } + + @Test + public void onlyOneInterceptorIsAvailable() { + Map interceptors = ctx.getBeansOfType(CacheInterceptor.class); + assertEquals("Only one interceptor should be defined", 1, interceptors.size()); + CacheInterceptor interceptor = interceptors.values().iterator().next(); + assertEquals("Custom interceptor not defined", TestCacheInterceptor.class, interceptor.getClass()); + } + + @Test + public void customInterceptorAppliesWithRuntimeException() { + Object o = cs.throwUnchecked(0L); + assertEquals(55L, o); // See TestCacheInterceptor + } + + @Test + public void customInterceptorAppliesWithCheckedException() { + try { + cs.throwChecked(0L); + fail("Should have failed"); + } + catch (RuntimeException e) { + assertNotNull("missing original exception", e.getCause()); + assertEquals(Exception.class, e.getCause().getClass()); + } + catch (Exception e) { + fail("Wrong exception type " + e); + } + } + + + @Configuration + @EnableCaching + static class EnableCachingConfig { + + @Bean + public CacheManager cacheManager() { + return CacheTestUtils.createSimpleCacheManager("default", "primary", "secondary"); + } + + @Bean + public CacheableService service() { + return new DefaultCacheableService(); + } + + @Bean + public CacheInterceptor cacheInterceptor(CacheOperationSource cacheOperationSource) { + CacheInterceptor cacheInterceptor = new TestCacheInterceptor(); + cacheInterceptor.setCacheManager(cacheManager()); + cacheInterceptor.setCacheOperationSources(cacheOperationSource); + return cacheInterceptor; + } + } + + /** + * A test {@link CacheInterceptor} that handles special exception + * types. + */ + static class TestCacheInterceptor extends CacheInterceptor { + + @Override + protected Object invokeOperation(CacheOperationInvoker invoker) { + try { + return super.invokeOperation(invoker); + } + catch (CacheOperationInvoker.ThrowableWrapper e) { + Throwable original = e.getOriginal(); + if (original.getClass() == UnsupportedOperationException.class) { + return 55L; + } + else { + throw new CacheOperationInvoker.ThrowableWrapper( + new RuntimeException("wrapping original", original)); + } + } + } + } + +}