Skip to content

Commit aaae10c

Browse files
committed
Cache operation invocation hook point
This commit adds a invokeOperation protected method in case one needs a hook point in the way the underlying cache method is invoked, and how exceptions that might be thrown by that invocation are handled. Issue: SPR-11540
1 parent c9d0ebd commit aaae10c

File tree

5 files changed

+350
-13
lines changed

5 files changed

+350
-13
lines changed

spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void afterPropertiesSet() {
8989
this.cacheResultInterceptor = new CacheResultInterceptor(getErrorHandler());
9090
this.cachePutInterceptor = new CachePutInterceptor(getErrorHandler());
9191
this.cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(getErrorHandler());
92-
this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler());
92+
this.cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(getErrorHandler());
9393

9494
this.initialized = true;
9595
}
@@ -130,26 +130,55 @@ private Class<?> getTargetClass(Object target) {
130130
@SuppressWarnings("unchecked")
131131
private Object execute(CacheOperationInvocationContext<?> context,
132132
CacheOperationInvoker invoker) {
133+
134+
CacheOperationInvoker adapter = new CacheOperationInvokerAdapter(invoker);
135+
133136
BasicCacheOperation operation = context.getOperation();
134137
if (operation instanceof CacheResultOperation) {
135138
return cacheResultInterceptor.invoke(
136-
(CacheOperationInvocationContext<CacheResultOperation>) context, invoker);
139+
(CacheOperationInvocationContext<CacheResultOperation>) context, adapter);
137140
}
138141
else if (operation instanceof CachePutOperation) {
139142
return cachePutInterceptor.invoke(
140-
(CacheOperationInvocationContext<CachePutOperation>) context, invoker);
143+
(CacheOperationInvocationContext<CachePutOperation>) context, adapter);
141144
}
142145
else if (operation instanceof CacheRemoveOperation) {
143146
return cacheRemoveEntryInterceptor.invoke(
144-
(CacheOperationInvocationContext<CacheRemoveOperation>) context, invoker);
147+
(CacheOperationInvocationContext<CacheRemoveOperation>) context, adapter);
145148
}
146149
else if (operation instanceof CacheRemoveAllOperation) {
147150
return cacheRemoveAllInterceptor.invoke(
148-
(CacheOperationInvocationContext<CacheRemoveAllOperation>) context, invoker);
151+
(CacheOperationInvocationContext<CacheRemoveAllOperation>) context, adapter);
149152
}
150153
else {
151154
throw new IllegalArgumentException("Could not handle " + operation);
152155
}
153156
}
154157

158+
/**
159+
* Execute the underlying operation (typically in case of cache miss) and return
160+
* the result of the invocation. If an exception occurs it will be wrapped in
161+
* a {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
162+
* or modified but it <em>must</em> be wrapped in a
163+
* {@link CacheOperationInvoker.ThrowableWrapper} as well.
164+
* @param invoker the invoker handling the operation being cached
165+
* @return the result of the invocation
166+
* @see CacheOperationInvoker#invoke()
167+
*/
168+
protected Object invokeOperation(CacheOperationInvoker invoker) {
169+
return invoker.invoke();
170+
}
171+
172+
private class CacheOperationInvokerAdapter implements CacheOperationInvoker {
173+
174+
private final CacheOperationInvoker delegate;
175+
176+
private CacheOperationInvokerAdapter(CacheOperationInvoker delegate) {this.delegate = delegate;}
177+
178+
@Override
179+
public Object invoke() throws ThrowableWrapper {
180+
return invokeOperation(delegate);
181+
}
182+
}
183+
155184
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cache.jcache.config;
18+
19+
import static org.junit.Assert.*;
20+
21+
import java.io.IOException;
22+
import java.util.Arrays;
23+
import java.util.Map;
24+
25+
import org.junit.After;
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
import org.springframework.cache.Cache;
30+
import org.springframework.cache.CacheManager;
31+
import org.springframework.cache.annotation.EnableCaching;
32+
import org.springframework.cache.concurrent.ConcurrentMapCache;
33+
import org.springframework.cache.interceptor.CacheOperationInvoker;
34+
import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService;
35+
import org.springframework.cache.jcache.interceptor.JCacheInterceptor;
36+
import org.springframework.cache.jcache.interceptor.JCacheOperationSource;
37+
import org.springframework.cache.support.SimpleCacheManager;
38+
import org.springframework.context.ConfigurableApplicationContext;
39+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
40+
import org.springframework.context.annotation.Bean;
41+
import org.springframework.context.annotation.Configuration;
42+
43+
/**
44+
*
45+
* @author Stephane Nicoll
46+
*/
47+
public class JCacheCustomInterceptorTests {
48+
49+
protected ConfigurableApplicationContext ctx;
50+
51+
protected JCacheableService<?> cs;
52+
53+
protected Cache exceptionCache;
54+
55+
@Before
56+
public void setup() {
57+
ctx = new AnnotationConfigApplicationContext(EnableCachingConfig.class);
58+
cs = ctx.getBean("service", JCacheableService.class);
59+
exceptionCache = ctx.getBean("exceptionCache", Cache.class);
60+
}
61+
62+
@After
63+
public void tearDown() {
64+
ctx.close();
65+
}
66+
67+
@Test
68+
public void onlyOneInterceptorIsAvailable() {
69+
Map<String, JCacheInterceptor> interceptors = ctx.getBeansOfType(JCacheInterceptor.class);
70+
assertEquals("Only one interceptor should be defined", 1, interceptors.size());
71+
JCacheInterceptor interceptor = interceptors.values().iterator().next();
72+
assertEquals("Custom interceptor not defined", TestCacheInterceptor.class, interceptor.getClass());
73+
}
74+
75+
@Test
76+
public void customInterceptorAppliesWithRuntimeException() {
77+
Object o = cs.cacheWithException("id", true);
78+
assertEquals(55L, o); // See TestCacheInterceptor
79+
}
80+
81+
@Test
82+
public void customInterceptorAppliesWithCheckedException() {
83+
try {
84+
cs.cacheWithCheckedException("id", true);
85+
fail("Should have failed");
86+
}
87+
catch (RuntimeException e) {
88+
assertNotNull("missing original exception", e.getCause());
89+
assertEquals(IOException.class, e.getCause().getClass());
90+
}
91+
catch (Exception e) {
92+
fail("Wrong exception type " + e);
93+
}
94+
}
95+
96+
97+
@Configuration
98+
@EnableCaching
99+
static class EnableCachingConfig {
100+
101+
@Bean
102+
public CacheManager cacheManager() {
103+
SimpleCacheManager cm = new SimpleCacheManager();
104+
cm.setCaches(Arrays.asList(
105+
defaultCache(),
106+
exceptionCache()));
107+
return cm;
108+
}
109+
110+
@Bean
111+
public JCacheableService<?> service() {
112+
return new AnnotatedJCacheableService(defaultCache());
113+
}
114+
115+
@Bean
116+
public Cache defaultCache() {
117+
return new ConcurrentMapCache("default");
118+
}
119+
120+
@Bean
121+
public Cache exceptionCache() {
122+
return new ConcurrentMapCache("exception");
123+
}
124+
125+
@Bean
126+
public JCacheInterceptor jCacheInterceptor(JCacheOperationSource cacheOperationSource) {
127+
JCacheInterceptor cacheInterceptor = new TestCacheInterceptor();
128+
cacheInterceptor.setCacheOperationSource(cacheOperationSource);
129+
return cacheInterceptor;
130+
}
131+
}
132+
133+
/**
134+
* A test {@link org.springframework.cache.interceptor.CacheInterceptor} that handles special exception
135+
* types.
136+
*/
137+
static class TestCacheInterceptor extends JCacheInterceptor {
138+
139+
@Override
140+
protected Object invokeOperation(CacheOperationInvoker invoker) {
141+
try {
142+
return super.invokeOperation(invoker);
143+
}
144+
catch (CacheOperationInvoker.ThrowableWrapper e) {
145+
Throwable original = e.getOriginal();
146+
if (original.getClass() == UnsupportedOperationException.class) {
147+
return 55L;
148+
}
149+
else {
150+
throw new CacheOperationInvoker.ThrowableWrapper(
151+
new RuntimeException("wrapping original", original));
152+
}
153+
}
154+
}
155+
}
156+
157+
}

spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,20 @@ protected Object execute(CacheOperationInvoker invoker, Object target, Method me
284284
return invoker.invoke();
285285
}
286286

287+
/**
288+
* Execute the underlying operation (typically in case of cache miss) and return
289+
* the result of the invocation. If an exception occurs it will be wrapped in
290+
* a {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
291+
* or modified but it <em>must</em> be wrapped in a
292+
* {@link CacheOperationInvoker.ThrowableWrapper} as well.
293+
* @param invoker the invoker handling the operation being cached
294+
* @return the result of the invocation
295+
* @see CacheOperationInvoker#invoke()
296+
*/
297+
protected Object invokeOperation(CacheOperationInvoker invoker) {
298+
return invoker.invoke();
299+
}
300+
287301
private Class<?> getTargetClass(Object target) {
288302
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
289303
if (targetClass == null && target != null) {
@@ -314,7 +328,7 @@ private Object execute(CacheOperationInvoker invoker, CacheOperationContexts con
314328

315329
// Invoke the method if don't have a cache hit
316330
if (result == null) {
317-
result = new SimpleValueWrapper(invoker.invoke());
331+
result = new SimpleValueWrapper(invokeOperation(invoker));
318332
}
319333

320334
// Collect any explicit @CachePuts

spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,22 @@
1919
/**
2020
* Abstract the invocation of a cache operation.
2121
*
22-
* <p>Provide a special exception that can be used to indicate that the
23-
* underlying invocation has thrown a checked exception, allowing the
24-
* callers to threat these in a different manner if necessary.
22+
* <p>Does not provide a way to transmit checked exceptions but
23+
* provide a special exception that should be used to wrap any
24+
* exception that was thrown by the underlying invocation. Callers
25+
* are expected to handle this issue type specifically.
2526
*
2627
* @author Stephane Nicoll
2728
* @since 4.1
2829
*/
2930
public interface CacheOperationInvoker {
3031

3132
/**
32-
* Invoke the cache operation defined by this instance. Can throw a
33-
* {@link ThrowableWrapper} if that operation wants to explicitly
34-
* indicate that a checked exception has occurred.
33+
* Invoke the cache operation defined by this instance. Wraps any
34+
* exception that is thrown during the invocation in a
35+
* {@link ThrowableWrapper}.
3536
* @return the result of the operation
36-
* @throws ThrowableWrapper if a checked exception has been thrown
37+
* @throws ThrowableWrapper if an error occurred while invoking the operation
3738
*/
3839
Object invoke() throws ThrowableWrapper;
3940

0 commit comments

Comments
 (0)