Skip to content

Commit 0194988

Browse files
committed
Store by value support for ConcurrentMapCacheManager
ConcurrentMapCacheManager and ConcurrentMapCache now support the serialization of cache entries via a new `storeByValue` attribute. If it is explicitly enabled, the cache value is first serialized and that content is stored in the cache. The net result is that any further change made on the object returned from the annotated method is not applied on the copy held in the cache. Issue: SPR-13758
1 parent cf20308 commit 0194988

File tree

5 files changed

+207
-4
lines changed

5 files changed

+207
-4
lines changed

spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616

1717
package org.springframework.cache.concurrent;
1818

19+
import java.io.ByteArrayInputStream;
20+
import java.io.ByteArrayOutputStream;
21+
import java.io.IOException;
1922
import java.util.concurrent.Callable;
2023
import java.util.concurrent.ConcurrentHashMap;
2124
import java.util.concurrent.ConcurrentMap;
2225

2326
import org.springframework.cache.support.AbstractValueAdaptingCache;
27+
import org.springframework.core.serializer.support.SerializationDelegate;
2428
import org.springframework.util.Assert;
2529

2630
/**
@@ -38,6 +42,7 @@
3842
*
3943
* @author Costin Leau
4044
* @author Juergen Hoeller
45+
* @author Stephane Nicoll
4146
* @since 3.1
4247
*/
4348
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
@@ -46,6 +51,8 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache {
4651

4752
private final ConcurrentMap<Object, Object> store;
4853

54+
private final SerializationDelegate serialization;
55+
4956

5057
/**
5158
* Create a new ConcurrentMapCache with the specified name.
@@ -74,13 +81,40 @@ public ConcurrentMapCache(String name, boolean allowNullValues) {
7481
* (adapting them to an internal null holder value)
7582
*/
7683
public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) {
84+
this(name, store, allowNullValues, null);
85+
}
86+
87+
/**
88+
* Create a new ConcurrentMapCache with the specified name and the
89+
* given internal {@link ConcurrentMap} to use. If the
90+
* {@link SerializationDelegate} is specified,
91+
* {@link #isStoreByValue() store-by-value} is enabled
92+
* @param name the name of the cache
93+
* @param store the ConcurrentMap to use as an internal store
94+
* @param allowNullValues whether to allow {@code null} values
95+
* (adapting them to an internal null holder value)
96+
* @param serialization the {@link SerializationDelegate} to use
97+
* to serialize cache entry or {@code null} to store the reference
98+
*/
99+
protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
100+
boolean allowNullValues, SerializationDelegate serialization) {
101+
77102
super(allowNullValues);
78103
Assert.notNull(name, "Name must not be null");
79104
Assert.notNull(store, "Store must not be null");
80105
this.name = name;
81106
this.store = store;
107+
this.serialization = serialization;
82108
}
83109

110+
/**
111+
* Return whether this cache stores a copy of each entry ({@code true}) or
112+
* a reference ({@code false}, default). If store by value is enabled, each
113+
* entry in the cache must be serializable.
114+
*/
115+
public final boolean isStoreByValue() {
116+
return this.serialization != null;
117+
}
84118

85119
@Override
86120
public final String getName() {
@@ -142,4 +176,59 @@ public void clear() {
142176
this.store.clear();
143177
}
144178

179+
@Override
180+
protected Object toStoreValue(Object userValue) {
181+
Object storeValue = super.toStoreValue(userValue);
182+
if (this.serialization != null) {
183+
try {
184+
return serializeValue(storeValue);
185+
}
186+
catch (Exception ex) {
187+
throw new IllegalArgumentException("Failed to serialize cache value '"
188+
+ userValue + "'. Does it implement Serializable?", ex);
189+
}
190+
}
191+
else {
192+
return storeValue;
193+
}
194+
}
195+
196+
private Object serializeValue(Object storeValue) throws IOException {
197+
ByteArrayOutputStream out = new ByteArrayOutputStream();
198+
try {
199+
this.serialization.serialize(storeValue, out);
200+
return out.toByteArray();
201+
}
202+
finally {
203+
out.close();
204+
}
205+
}
206+
207+
@Override
208+
protected Object fromStoreValue(Object storeValue) {
209+
if (this.serialization != null) {
210+
try {
211+
return super.fromStoreValue(deserializeValue(storeValue));
212+
}
213+
catch (Exception ex) {
214+
throw new IllegalArgumentException("Failed to deserialize cache value '" +
215+
storeValue + "'", ex);
216+
}
217+
}
218+
else {
219+
return super.fromStoreValue(storeValue);
220+
}
221+
222+
}
223+
224+
private Object deserializeValue(Object storeValue) throws IOException {
225+
ByteArrayInputStream in = new ByteArrayInputStream((byte[]) storeValue);
226+
try {
227+
return this.serialization.deserialize(in);
228+
}
229+
finally {
230+
in.close();
231+
}
232+
}
233+
145234
}

spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
import java.util.concurrent.ConcurrentHashMap;
2424
import java.util.concurrent.ConcurrentMap;
2525

26+
import org.springframework.beans.factory.BeanClassLoaderAware;
2627
import org.springframework.cache.Cache;
2728
import org.springframework.cache.CacheManager;
29+
import org.springframework.core.serializer.support.SerializationDelegate;
2830

2931
/**
3032
* {@link CacheManager} implementation that lazily builds {@link ConcurrentMapCache}
@@ -44,14 +46,18 @@
4446
* @since 3.1
4547
* @see ConcurrentMapCache
4648
*/
47-
public class ConcurrentMapCacheManager implements CacheManager {
49+
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
4850

4951
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
5052

5153
private boolean dynamic = true;
5254

5355
private boolean allowNullValues = true;
5456

57+
private boolean storeByValue = false;
58+
59+
private SerializationDelegate serialization;
60+
5561

5662
/**
5763
* Construct a dynamic ConcurrentMapCacheManager,
@@ -114,6 +120,37 @@ public boolean isAllowNullValues() {
114120
return this.allowNullValues;
115121
}
116122

123+
/**
124+
* Specify whether this cache manager stores a copy of each entry ({@code true}
125+
* or the reference ({@code false} for all of its caches.
126+
* <p>Default is "false" so that the value itself is stored and no serializable
127+
* contract is required on cached values.
128+
* <p>Note: A change of the store-by-value setting will reset all existing caches,
129+
* if any, to reconfigure them with the new store-by-value requirement.
130+
*/
131+
public void setStoreByValue(boolean storeByValue) {
132+
if (storeByValue != this.storeByValue) {
133+
this.storeByValue = storeByValue;
134+
// Need to recreate all Cache instances with the new store-by-value configuration...
135+
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
136+
entry.setValue(createConcurrentMapCache(entry.getKey()));
137+
}
138+
}
139+
}
140+
141+
/**
142+
* Return whether this cache manager stores a copy of each entry or
143+
* a reference for all its caches. If store by value is enabled, any
144+
* cache entry must be serializable.
145+
*/
146+
public boolean isStoreByValue() {
147+
return this.storeByValue;
148+
}
149+
150+
@Override
151+
public void setBeanClassLoader(ClassLoader classLoader) {
152+
this.serialization = new SerializationDelegate(classLoader);
153+
}
117154

118155
@Override
119156
public Collection<String> getCacheNames() {
@@ -141,7 +178,11 @@ public Cache getCache(String name) {
141178
* @return the ConcurrentMapCache (or a decorator thereof)
142179
*/
143180
protected Cache createConcurrentMapCache(String name) {
144-
return new ConcurrentMapCache(name, isAllowNullValues());
181+
SerializationDelegate actualSerialization =
182+
this.storeByValue ? serialization : null;
183+
return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256),
184+
isAllowNullValues(), actualSerialization);
185+
145186
}
146187

147188
}

spring-context/src/test/java/org/springframework/cache/AbstractCacheTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ public void testCacheGetSynchronized() throws InterruptedException {
211211
results.forEach(r -> assertThat(r, is(1))); // Only one method got invoked
212212
}
213213

214-
private String createRandomKey() {
214+
protected String createRandomKey() {
215215
return UUID.randomUUID().toString();
216216
}
217217

spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
/**
2727
* @author Juergen Hoeller
28+
* @author Stephane Nicoll
2829
*/
2930
public class ConcurrentMapCacheManagerTests {
3031

@@ -120,4 +121,21 @@ public void testStaticMode() {
120121
assertNull(cache1y.get("key3"));
121122
}
122123

124+
@Test
125+
public void testChangeStoreByValue() {
126+
ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2");
127+
assertFalse(cm.isStoreByValue());
128+
Cache cache1 = cm.getCache("c1");
129+
assertTrue(cache1 instanceof ConcurrentMapCache);
130+
assertFalse(((ConcurrentMapCache)cache1).isStoreByValue());
131+
cache1.put("key", "value");
132+
133+
cm.setStoreByValue(true);
134+
assertTrue(cm.isStoreByValue());
135+
Cache cache1x = cm.getCache("c1");
136+
assertTrue(cache1x instanceof ConcurrentMapCache);
137+
assertTrue(cache1x != cache1);
138+
assertNull(cache1x.get("key"));
139+
}
140+
123141
}

spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@
1616

1717
package org.springframework.cache.concurrent;
1818

19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.List;
1922
import java.util.concurrent.ConcurrentHashMap;
2023
import java.util.concurrent.ConcurrentMap;
2124

2225
import org.junit.Before;
23-
import org.junit.Ignore;
26+
import org.junit.Test;
2427

2528
import org.springframework.cache.AbstractCacheTests;
29+
import org.springframework.core.serializer.support.SerializationDelegate;
30+
31+
import static org.junit.Assert.*;
2632

2733
/**
2834
* @author Costin Leau
@@ -53,4 +59,53 @@ protected ConcurrentMap<Object, Object> getNativeCache() {
5359
return this.nativeCache;
5460
}
5561

62+
@Test
63+
public void testIsStoreByReferenceByDefault() {
64+
assertFalse(this.cache.isStoreByValue());
65+
}
66+
67+
@SuppressWarnings("unchecked")
68+
@Test
69+
public void testSerializer() {
70+
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
71+
assertTrue(serializeCache.isStoreByValue());
72+
73+
Object key = createRandomKey();
74+
List<String> content = new ArrayList<>();
75+
content.addAll(Arrays.asList("one", "two", "three"));
76+
serializeCache.put(key, content);
77+
content.remove(0);
78+
List<String> entry = (List<String>) serializeCache.get(key).get();
79+
assertEquals(3, entry.size());
80+
assertEquals("one", entry.get(0));
81+
}
82+
83+
@Test
84+
public void testNonSerializableContent() {
85+
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
86+
87+
thrown.expect(IllegalArgumentException.class);
88+
thrown.expectMessage("Failed to serialize");
89+
thrown.expectMessage(this.cache.getClass().getName());
90+
serializeCache.put(createRandomKey(), this.cache);
91+
}
92+
93+
@Test
94+
public void testInvalidSerializedContent() {
95+
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
96+
97+
String key = createRandomKey();
98+
this.nativeCache.put(key, "Some garbage");
99+
thrown.expect(IllegalArgumentException.class);
100+
thrown.expectMessage("Failed to deserialize");
101+
thrown.expectMessage("Some garbage");
102+
serializeCache.get(key);
103+
}
104+
105+
106+
private ConcurrentMapCache createCacheWithStoreByValue() {
107+
return new ConcurrentMapCache(CACHE_NAME, nativeCache, true,
108+
new SerializationDelegate(ConcurrentMapCacheTests.class.getClassLoader()));
109+
}
110+
56111
}

0 commit comments

Comments
 (0)