Skip to content

Commit 19d97c4

Browse files
committed
Support for multi-threaded cache access
Previously, if a `@Cacheable` method was accessed with the same key by multiple threads, the underlying method was invoked several times instead of blocking the threads while the value is computed. This scenario typically affects users that enable caching to avoid calling a costly method too often. When said method can be invoked by an arbitrary number of clients on startup, caching has close to no effect. This commit adds a new method on `Cache` that implements the read-through pattern: ``` <T> T get(Object key, Callable<T> valueLoader); ``` If an entry for a given key is not found, the specified `Callable` is invoked to "load" the value and cache it before returning it to the caller. Because the entire operation is managed by the underlying cache provider, it is much more easier to guarantee that the loader (e.g. the annotated method) will be called only once in case of concurrent access. A new `sync` attribute to the `@Cacheable` annotation has been addded. When this flag is enabled, the caching abstraction invokes the new `Cache` method define above. This new mode bring a set of limitations: * It can't be combined with other cache operations * Only one `@Cacheable` operation can be specified * Only one cache is allowed * `condition` and `unless` attribute are not supported The rationale behind those limitations is that the underlying Cache is taking care of the actual caching operation so we can't really apply any SpEL or multiple caches handling there. Issue: SPR-9254
1 parent 15c7dcd commit 19d97c4

File tree

33 files changed

+938
-146
lines changed

33 files changed

+938
-146
lines changed

spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,22 @@ public abstract aspect AbstractCacheAspect extends CacheAspectSupport implements
6767

6868
CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() {
6969
public Object invoke() {
70-
return proceed(cachedObject);
70+
try {
71+
return proceed(cachedObject);
72+
}
73+
catch (Throwable ex) {
74+
throw new ThrowableWrapper(ex);
75+
}
7176
}
7277
};
7378

74-
return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs());
79+
try {
80+
return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs());
81+
}
82+
catch (CacheOperationInvoker.ThrowableWrapper th) {
83+
AnyThrow.throwUnchecked(th.getOriginal());
84+
return null; // never reached
85+
}
7586
}
7687

7788
/**
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2002-2015 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.aspectj;
18+
19+
/**
20+
* Utility to trick the compiler to throw a valid checked
21+
* exceptions within the interceptor.
22+
*
23+
* @author Stephane Nicoll
24+
*/
25+
class AnyThrow {
26+
27+
static void throwUnchecked(Throwable e) {
28+
AnyThrow.<RuntimeException>throwAny(e);
29+
}
30+
31+
@SuppressWarnings("unchecked")
32+
private static <E extends Throwable> void throwAny(Throwable e) throws E {
33+
throw (E) e;
34+
}
35+
}

spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,4 @@ public aspect JCacheCacheAspect extends JCacheAspectSupport {
109109
execution(@CacheRemoveAll * *(..));
110110

111111

112-
private static class AnyThrow {
113-
114-
private static void throwUnchecked(Throwable e) {
115-
AnyThrow.<RuntimeException>throwAny(e);
116-
}
117-
118-
@SuppressWarnings("unchecked")
119-
private static <E extends Throwable> void throwAny(Throwable e) throws E {
120-
throw (E)e;
121-
}
122-
}
123-
124112
}

spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
/**
2828
* @author Costin Leau
2929
* @author Phillip Webb
30+
* @author Stephane Nicoll
3031
*/
3132
@Cacheable("testCache")
3233
public class AnnotatedClassCacheableService implements CacheableService<Object> {
@@ -44,11 +45,28 @@ public Object cacheNull(Object arg1) {
4445
return null;
4546
}
4647

48+
@Override
49+
@Cacheable(cacheNames = "testCache", sync = true)
50+
public Object cacheSync(Object arg1) {
51+
return counter.getAndIncrement();
52+
}
53+
54+
@Override
55+
@Cacheable(cacheNames = "testCache", sync = true)
56+
public Object cacheSyncNull(Object arg1) {
57+
return null;
58+
}
59+
4760
@Override
4861
public Object conditional(int field) {
4962
return null;
5063
}
5164

65+
@Override
66+
public Object conditionalSync(int field) {
67+
return null;
68+
}
69+
5270
@Override
5371
public Object unless(int arg) {
5472
return arg;
@@ -168,6 +186,18 @@ public Long throwUnchecked(Object arg1) {
168186
throw new UnsupportedOperationException(arg1.toString());
169187
}
170188

189+
@Override
190+
@Cacheable(cacheNames = "testCache", sync = true)
191+
public Object throwCheckedSync(Object arg1) throws Exception {
192+
throw new IOException(arg1.toString());
193+
}
194+
195+
@Override
196+
@Cacheable(cacheNames = "testCache", sync = true)
197+
public Object throwUncheckedSync(Object arg1) {
198+
throw new UnsupportedOperationException(arg1.toString());
199+
}
200+
171201
// multi annotations
172202

173203
@Override

spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
*
2222
* @author Costin Leau
2323
* @author Phillip Webb
24+
* @author Stephane Nicoll
2425
*/
2526
public interface CacheableService<T> {
2627

2728
T cache(Object arg1);
2829

2930
T cacheNull(Object arg1);
3031

32+
T cacheSync(Object arg1);
33+
34+
T cacheSyncNull(Object arg1);
35+
3136
void invalidate(Object arg1);
3237

3338
void evictEarly(Object arg1);
@@ -42,6 +47,8 @@ public interface CacheableService<T> {
4247

4348
T conditional(int field);
4449

50+
T conditionalSync(int field);
51+
4552
T unless(int arg);
4653

4754
T key(Object arg1, Object arg2);
@@ -72,6 +79,10 @@ public interface CacheableService<T> {
7279

7380
T throwUnchecked(Object arg1);
7481

82+
T throwCheckedSync(Object arg1) throws Exception;
83+
84+
T throwUncheckedSync(Object arg1);
85+
7586
// multi annotations
7687
T multiCache(Object arg1);
7788

spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* @author Costin Leau
3131
* @author Phillip Webb
32+
* @author Stephane Nicoll
3233
*/
3334
public class DefaultCacheableService implements CacheableService<Long> {
3435

@@ -47,6 +48,18 @@ public Long cacheNull(Object arg1) {
4748
return null;
4849
}
4950

51+
@Override
52+
@Cacheable(cacheNames = "testCache", sync = true)
53+
public Long cacheSync(Object arg1) {
54+
return counter.getAndIncrement();
55+
}
56+
57+
@Override
58+
@Cacheable(cacheNames = "testCache", sync = true)
59+
public Long cacheSyncNull(Object arg1) {
60+
return null;
61+
}
62+
5063
@Override
5164
@CacheEvict("testCache")
5265
public void invalidate(Object arg1) {
@@ -81,11 +94,17 @@ public void invalidateEarly(Object arg1, Object arg2) {
8194
}
8295

8396
@Override
84-
@Cacheable(cacheNames = "testCache", condition = "#classField == 3")
97+
@Cacheable(cacheNames = "testCache", condition = "#p0 == 3")
8598
public Long conditional(int classField) {
8699
return counter.getAndIncrement();
87100
}
88101

102+
@Override
103+
@Cacheable(cacheNames = "testCache", sync = true, condition = "#p0 == 3")
104+
public Long conditionalSync(int field) {
105+
return counter.getAndIncrement();
106+
}
107+
89108
@Override
90109
@Cacheable(cacheNames = "testCache", unless = "#result > 10")
91110
public Long unless(int arg) {
@@ -99,7 +118,7 @@ public Long key(Object arg1, Object arg2) {
99118
}
100119

101120
@Override
102-
@Cacheable("testCache")
121+
@Cacheable(cacheNames = "testCache")
103122
public Long varArgsKey(Object... args) {
104123
return counter.getAndIncrement();
105124
}
@@ -176,6 +195,18 @@ public Long throwUnchecked(Object arg1) {
176195
throw new UnsupportedOperationException(arg1.toString());
177196
}
178197

198+
@Override
199+
@Cacheable(cacheNames = "testCache", sync = true)
200+
public Long throwCheckedSync(Object arg1) throws Exception {
201+
throw new IOException(arg1.toString());
202+
}
203+
204+
@Override
205+
@Cacheable(cacheNames = "testCache", sync = true)
206+
public Long throwUncheckedSync(Object arg1) {
207+
throw new UnsupportedOperationException(arg1.toString());
208+
}
209+
179210
// multi annotations
180211

181212
@Override

spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.cache.caffeine;
1818

19+
import java.util.concurrent.Callable;
1920
import java.util.function.Function;
2021

2122
import com.github.benmanes.caffeine.cache.LoadingCache;
@@ -88,6 +89,12 @@ public ValueWrapper get(Object key) {
8889
return super.get(key);
8990
}
9091

92+
@SuppressWarnings("unchecked")
93+
@Override
94+
public <T> T get(Object key, final Callable<T> valueLoader) {
95+
return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader)));
96+
}
97+
9198
@Override
9299
protected Object lookup(Object key) {
93100
return this.cache.getIfPresent(key);
@@ -133,4 +140,23 @@ public Object apply(Object key) {
133140
}
134141
}
135142

143+
private class LoadFunction implements Function<Object, Object> {
144+
145+
private final Callable<?> valueLoader;
146+
147+
public LoadFunction(Callable<?> valueLoader) {
148+
this.valueLoader = valueLoader;
149+
}
150+
151+
@Override
152+
public Object apply(Object o) {
153+
try {
154+
return toStoreValue(valueLoader.call());
155+
}
156+
catch (Exception ex) {
157+
throw new ValueRetrievalException(o, valueLoader, ex);
158+
}
159+
}
160+
}
161+
136162
}

spring-context-support/src/main/java/org/springframework/cache/ehcache/EhCacheCache.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cache.ehcache;
1818

19+
import java.util.concurrent.Callable;
20+
1921
import net.sf.ehcache.Ehcache;
2022
import net.sf.ehcache.Element;
2123
import net.sf.ehcache.Status;
@@ -62,10 +64,47 @@ public final Ehcache getNativeCache() {
6264

6365
@Override
6466
public ValueWrapper get(Object key) {
65-
Element element = this.cache.get(key);
67+
Element element = lookup(key);
6668
return toValueWrapper(element);
6769
}
6870

71+
@SuppressWarnings("unchecked")
72+
@Override
73+
public <T> T get(Object key, Callable<T> valueLoader) {
74+
Element element = lookup(key);
75+
if (element != null) {
76+
return (T) element.getObjectValue();
77+
}
78+
else {
79+
this.cache.acquireWriteLockOnKey(key);
80+
try {
81+
element = lookup(key); // One more attempt with the write lock
82+
if (element != null) {
83+
return (T) element.getObjectValue();
84+
}
85+
else {
86+
return loadValue(key, valueLoader);
87+
}
88+
}
89+
finally {
90+
this.cache.releaseWriteLockOnKey(key);
91+
}
92+
}
93+
94+
}
95+
96+
private <T> T loadValue(Object key, Callable<T> valueLoader) {
97+
T value;
98+
try {
99+
value = valueLoader.call();
100+
}
101+
catch (Exception ex) {
102+
throw new ValueRetrievalException(key, valueLoader, ex);
103+
}
104+
put(key, value);
105+
return value;
106+
}
107+
69108
@Override
70109
@SuppressWarnings("unchecked")
71110
public <T> T get(Object key, Class<T> type) {
@@ -98,6 +137,11 @@ public void clear() {
98137
this.cache.removeAll();
99138
}
100139

140+
141+
private Element lookup(Object key) {
142+
return this.cache.get(key);
143+
}
144+
101145
private ValueWrapper toValueWrapper(Element element) {
102146
return (element != null ? new SimpleValueWrapper(element.getObjectValue()) : null);
103147
}

spring-context-support/src/main/java/org/springframework/cache/guava/GuavaCache.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,25 @@ public ValueWrapper get(Object key) {
9393
return super.get(key);
9494
}
9595

96+
@SuppressWarnings("unchecked")
97+
@Override
98+
public <T> T get(Object key, final Callable<T> valueLoader) {
99+
try {
100+
return (T) fromStoreValue(this.cache.get(key, new Callable<Object>() {
101+
@Override
102+
public Object call() throws Exception {
103+
return toStoreValue(valueLoader.call());
104+
}
105+
}));
106+
}
107+
catch (ExecutionException ex) {
108+
throw new ValueRetrievalException(key, valueLoader, ex.getCause());
109+
}
110+
catch (UncheckedExecutionException ex) {
111+
throw new ValueRetrievalException(key, valueLoader, ex.getCause());
112+
}
113+
}
114+
96115
@Override
97116
protected Object lookup(Object key) {
98117
return this.cache.getIfPresent(key);

0 commit comments

Comments
 (0)