Browse Source

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
pull/912/merge
Stephane Nicoll 9 years ago
parent
commit
0194988425
  1. 89
      spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java
  2. 45
      spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java
  3. 2
      spring-context/src/test/java/org/springframework/cache/AbstractCacheTests.java
  4. 18
      spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java
  5. 57
      spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheTests.java

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

@ -16,11 +16,15 @@ @@ -16,11 +16,15 @@
package org.springframework.cache.concurrent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.core.serializer.support.SerializationDelegate;
import org.springframework.util.Assert;
/**
@ -38,6 +42,7 @@ import org.springframework.util.Assert; @@ -38,6 +42,7 @@ import org.springframework.util.Assert;
*
* @author Costin Leau
* @author Juergen Hoeller
* @author Stephane Nicoll
* @since 3.1
*/
public class ConcurrentMapCache extends AbstractValueAdaptingCache {
@ -46,6 +51,8 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { @@ -46,6 +51,8 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache {
private final ConcurrentMap<Object, Object> store;
private final SerializationDelegate serialization;
/**
* Create a new ConcurrentMapCache with the specified name.
@ -74,13 +81,40 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { @@ -74,13 +81,40 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache {
* (adapting them to an internal null holder value)
*/
public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) {
this(name, store, allowNullValues, null);
}
/**
* Create a new ConcurrentMapCache with the specified name and the
* given internal {@link ConcurrentMap} to use. If the
* {@link SerializationDelegate} is specified,
* {@link #isStoreByValue() store-by-value} is enabled
* @param name the name of the cache
* @param store the ConcurrentMap to use as an internal store
* @param allowNullValues whether to allow {@code null} values
* (adapting them to an internal null holder value)
* @param serialization the {@link SerializationDelegate} to use
* to serialize cache entry or {@code null} to store the reference
*/
protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store,
boolean allowNullValues, SerializationDelegate serialization) {
super(allowNullValues);
Assert.notNull(name, "Name must not be null");
Assert.notNull(store, "Store must not be null");
this.name = name;
this.store = store;
this.serialization = serialization;
}
/**
* Return whether this cache stores a copy of each entry ({@code true}) or
* a reference ({@code false}, default). If store by value is enabled, each
* entry in the cache must be serializable.
*/
public final boolean isStoreByValue() {
return this.serialization != null;
}
@Override
public final String getName() {
@ -142,4 +176,59 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { @@ -142,4 +176,59 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache {
this.store.clear();
}
@Override
protected Object toStoreValue(Object userValue) {
Object storeValue = super.toStoreValue(userValue);
if (this.serialization != null) {
try {
return serializeValue(storeValue);
}
catch (Exception ex) {
throw new IllegalArgumentException("Failed to serialize cache value '"
+ userValue + "'. Does it implement Serializable?", ex);
}
}
else {
return storeValue;
}
}
private Object serializeValue(Object storeValue) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
this.serialization.serialize(storeValue, out);
return out.toByteArray();
}
finally {
out.close();
}
}
@Override
protected Object fromStoreValue(Object storeValue) {
if (this.serialization != null) {
try {
return super.fromStoreValue(deserializeValue(storeValue));
}
catch (Exception ex) {
throw new IllegalArgumentException("Failed to deserialize cache value '" +
storeValue + "'", ex);
}
}
else {
return super.fromStoreValue(storeValue);
}
}
private Object deserializeValue(Object storeValue) throws IOException {
ByteArrayInputStream in = new ByteArrayInputStream((byte[]) storeValue);
try {
return this.serialization.deserialize(in);
}
finally {
in.close();
}
}
}

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

@ -23,8 +23,10 @@ import java.util.Map; @@ -23,8 +23,10 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.serializer.support.SerializationDelegate;
/**
* {@link CacheManager} implementation that lazily builds {@link ConcurrentMapCache}
@ -44,7 +46,7 @@ import org.springframework.cache.CacheManager; @@ -44,7 +46,7 @@ import org.springframework.cache.CacheManager;
* @since 3.1
* @see ConcurrentMapCache
*/
public class ConcurrentMapCacheManager implements CacheManager {
public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(16);
@ -52,6 +54,10 @@ public class ConcurrentMapCacheManager implements CacheManager { @@ -52,6 +54,10 @@ public class ConcurrentMapCacheManager implements CacheManager {
private boolean allowNullValues = true;
private boolean storeByValue = false;
private SerializationDelegate serialization;
/**
* Construct a dynamic ConcurrentMapCacheManager,
@ -114,6 +120,37 @@ public class ConcurrentMapCacheManager implements CacheManager { @@ -114,6 +120,37 @@ public class ConcurrentMapCacheManager implements CacheManager {
return this.allowNullValues;
}
/**
* Specify whether this cache manager stores a copy of each entry ({@code true}
* or the reference ({@code false} for all of its caches.
* <p>Default is "false" so that the value itself is stored and no serializable
* contract is required on cached values.
* <p>Note: A change of the store-by-value setting will reset all existing caches,
* if any, to reconfigure them with the new store-by-value requirement.
*/
public void setStoreByValue(boolean storeByValue) {
if (storeByValue != this.storeByValue) {
this.storeByValue = storeByValue;
// Need to recreate all Cache instances with the new store-by-value configuration...
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
entry.setValue(createConcurrentMapCache(entry.getKey()));
}
}
}
/**
* Return whether this cache manager stores a copy of each entry or
* a reference for all its caches. If store by value is enabled, any
* cache entry must be serializable.
*/
public boolean isStoreByValue() {
return this.storeByValue;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.serialization = new SerializationDelegate(classLoader);
}
@Override
public Collection<String> getCacheNames() {
@ -141,7 +178,11 @@ public class ConcurrentMapCacheManager implements CacheManager { @@ -141,7 +178,11 @@ public class ConcurrentMapCacheManager implements CacheManager {
* @return the ConcurrentMapCache (or a decorator thereof)
*/
protected Cache createConcurrentMapCache(String name) {
return new ConcurrentMapCache(name, isAllowNullValues());
SerializationDelegate actualSerialization =
this.storeByValue ? serialization : null;
return new ConcurrentMapCache(name, new ConcurrentHashMap<Object, Object>(256),
isAllowNullValues(), actualSerialization);
}
}

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

@ -211,7 +211,7 @@ public abstract class AbstractCacheTests<T extends Cache> { @@ -211,7 +211,7 @@ public abstract class AbstractCacheTests<T extends Cache> {
results.forEach(r -> assertThat(r, is(1))); // Only one method got invoked
}
private String createRandomKey() {
protected String createRandomKey() {
return UUID.randomUUID().toString();
}

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

@ -25,6 +25,7 @@ import static org.junit.Assert.*; @@ -25,6 +25,7 @@ import static org.junit.Assert.*;
/**
* @author Juergen Hoeller
* @author Stephane Nicoll
*/
public class ConcurrentMapCacheManagerTests {
@ -120,4 +121,21 @@ public class ConcurrentMapCacheManagerTests { @@ -120,4 +121,21 @@ public class ConcurrentMapCacheManagerTests {
assertNull(cache1y.get("key3"));
}
@Test
public void testChangeStoreByValue() {
ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager("c1", "c2");
assertFalse(cm.isStoreByValue());
Cache cache1 = cm.getCache("c1");
assertTrue(cache1 instanceof ConcurrentMapCache);
assertFalse(((ConcurrentMapCache)cache1).isStoreByValue());
cache1.put("key", "value");
cm.setStoreByValue(true);
assertTrue(cm.isStoreByValue());
Cache cache1x = cm.getCache("c1");
assertTrue(cache1x instanceof ConcurrentMapCache);
assertTrue(cache1x != cache1);
assertNull(cache1x.get("key"));
}
}

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

@ -16,13 +16,19 @@ @@ -16,13 +16,19 @@
package org.springframework.cache.concurrent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.cache.AbstractCacheTests;
import org.springframework.core.serializer.support.SerializationDelegate;
import static org.junit.Assert.*;
/**
* @author Costin Leau
@ -53,4 +59,53 @@ public class ConcurrentMapCacheTests extends AbstractCacheTests<ConcurrentMapCac @@ -53,4 +59,53 @@ public class ConcurrentMapCacheTests extends AbstractCacheTests<ConcurrentMapCac
return this.nativeCache;
}
@Test
public void testIsStoreByReferenceByDefault() {
assertFalse(this.cache.isStoreByValue());
}
@SuppressWarnings("unchecked")
@Test
public void testSerializer() {
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
assertTrue(serializeCache.isStoreByValue());
Object key = createRandomKey();
List<String> content = new ArrayList<>();
content.addAll(Arrays.asList("one", "two", "three"));
serializeCache.put(key, content);
content.remove(0);
List<String> entry = (List<String>) serializeCache.get(key).get();
assertEquals(3, entry.size());
assertEquals("one", entry.get(0));
}
@Test
public void testNonSerializableContent() {
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Failed to serialize");
thrown.expectMessage(this.cache.getClass().getName());
serializeCache.put(createRandomKey(), this.cache);
}
@Test
public void testInvalidSerializedContent() {
ConcurrentMapCache serializeCache = createCacheWithStoreByValue();
String key = createRandomKey();
this.nativeCache.put(key, "Some garbage");
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Failed to deserialize");
thrown.expectMessage("Some garbage");
serializeCache.get(key);
}
private ConcurrentMapCache createCacheWithStoreByValue() {
return new ConcurrentMapCache(CACHE_NAME, nativeCache, true,
new SerializationDelegate(ConcurrentMapCacheTests.class.getClassLoader()));
}
}

Loading…
Cancel
Save