DATAREDIS-553 - Support caching null values via RedisCache.

We now support caching `null` values in `RedisCache` by storing a dedicated `NullValue` reference as a placeholder. To enable this feature please set up `RedisCacheManager` accordingly and make sure the used `RedisSerializer` is capable of dealing the `NullValue` type.
Both the `JdkSerializationRedisSerializer` and the `GenericJackson2JsonRedisSerializer` support this out of the box.

Original pull request: #221.
This commit is contained in:
Christoph Strobl
2016-09-16 11:09:42 +02:00
committed by Mark Paluch
parent 72d6df00c8
commit 4c54bf972a
8 changed files with 259 additions and 28 deletions

View File

@@ -16,12 +16,8 @@
package org.springframework.data.redis.cache;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.springframework.data.redis.matcher.RedisTestMatchers.isEqual;
import static org.junit.Assert.*;
import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
import org.junit.Before;
import org.junit.Test;
@@ -38,22 +34,31 @@ public abstract class AbstractNativeCacheTest<T> {
private T nativeCache;
protected Cache cache;
protected final static String CACHE_NAME = "testCache";
private final boolean allowCacheNullValues;
protected AbstractNativeCacheTest(boolean allowCacheNullValues) {
this.allowCacheNullValues = allowCacheNullValues;
}
@Before
public void setUp() throws Exception {
nativeCache = createNativeCache();
cache = createCache(nativeCache);
cache = createCache(nativeCache, allowCacheNullValues);
cache.clear();
}
protected abstract T createNativeCache() throws Exception;
protected abstract Cache createCache(T nativeCache);
protected abstract Cache createCache(T nativeCache, boolean allowCacheNullValues);
protected abstract Object getKey();
protected abstract Object getValue();
protected boolean getAllowCacheNullValues() {
return allowCacheNullValues;
}
@Test
public void testCacheName() throws Exception {
assertEquals(CACHE_NAME, cache.getName());

View File

@@ -27,6 +27,8 @@ import static org.junit.Assert.*;
import static org.junit.Assume.*;
import static org.springframework.data.redis.matcher.RedisTestMatchers.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
@@ -34,6 +36,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.AfterClass;
import org.junit.AssumptionViolatedException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,6 +49,8 @@ import org.springframework.data.redis.ObjectFactory;
import org.springframework.data.redis.StringObjectFactory;
import org.springframework.data.redis.core.AbstractOperationsTestParams;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import edu.umd.cs.mtc.MultithreadedTestCase;
@@ -63,7 +68,11 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
private ObjectFactory<Object> valueFactory;
private RedisTemplate template;
public RedisCacheTest(RedisTemplate template, ObjectFactory<Object> keyFactory, ObjectFactory<Object> valueFactory) {
public RedisCacheTest(RedisTemplate template, ObjectFactory<Object> keyFactory, ObjectFactory<Object> valueFactory,
boolean allowCacheNullValues) {
super(allowCacheNullValues);
this.keyFactory = keyFactory;
this.valueFactory = valueFactory;
this.template = template;
@@ -72,12 +81,30 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
@Parameters
public static Collection<Object[]> testParams() {
return AbstractOperationsTestParams.testParams();
Collection<Object[]> params = AbstractOperationsTestParams.testParams();
Collection<Object[]> target = new ArrayList<Object[]>();
for (Object[] source : params) {
Object[] cacheNullDisabled = Arrays.copyOf(source, source.length + 1);
Object[] cacheNullEnabled = Arrays.copyOf(source, source.length + 1);
cacheNullDisabled[source.length] = false;
cacheNullEnabled[source.length] = true;
target.add(cacheNullDisabled);
target.add(cacheNullEnabled);
}
return target;
}
@SuppressWarnings("unchecked")
protected RedisCache createCache(RedisTemplate nativeCache) {
return new RedisCache(CACHE_NAME, CACHE_NAME.concat(":").getBytes(), nativeCache, TimeUnit.MINUTES.toSeconds(10));
protected RedisCache createCache(RedisTemplate nativeCache, boolean allowCacheNullValues) {
return new RedisCache(CACHE_NAME, CACHE_NAME.concat(":").getBytes(), nativeCache, TimeUnit.MINUTES.toSeconds(10),
allowCacheNullValues);
}
protected RedisTemplate createNativeCache() throws Exception {
@@ -86,6 +113,13 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
@Before
public void setUp() throws Exception {
if (!(template.getValueSerializer() instanceof JdkSerializationRedisSerializer
|| template.getValueSerializer() instanceof GenericJackson2JsonRedisSerializer
|| template.getValueSerializer() == null) && getAllowCacheNullValues()) {
throw new AssumptionViolatedException(
"Null values can only be cachend with the Jdk or GenericJackson2 serialization");
}
ConnectionFactoryTracker.add(template.getConnectionFactory());
super.setUp();
}
@@ -288,6 +322,8 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
@Test
public void cachePutWithNullShouldNotAddStuffToRedis() {
assumeThat(getAllowCacheNullValues(), is(false));
Object key = getKey();
Object value = getValue();
@@ -302,6 +338,8 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
@Test
public void cachePutWithNullShouldRemoveKeyIfExists() {
assumeThat(getAllowCacheNullValues(), is(false));
Object key = getKey();
Object value = getValue();
@@ -327,6 +365,22 @@ public class RedisCacheTest extends AbstractNativeCacheTest<RedisTemplate> {
runOnce(new CacheGetWithValueLoaderIsThreadSafe((RedisCache) cache));
}
/**
* @see DATAREDIS-553
*/
@Test
public void cachePutWithNullShouldAddStuffToRedisWhenCachingNullIsEnabled() {
assumeThat(getAllowCacheNullValues(), is(true));
Object key = getKey();
Object value = getValue();
cache.put(key, null);
assertThat(cache.get(key, String.class), is(nullValue()));
}
@SuppressWarnings("unused")
private static class CacheGetWithValueLoaderIsThreadSafe extends MultithreadedTestCase {

View File

@@ -250,7 +250,7 @@ public class RedisCacheUnitTests {
}
});
verify(connectionMock, times(2)).get(eq(KEY_BYTES));
verify(connectionMock, times(1)).get(eq(KEY_BYTES));
verify(connectionMock, times(1)).multi();
verify(connectionMock, times(1)).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(connectionMock, times(1)).exec();
@@ -267,7 +267,7 @@ public class RedisCacheUnitTests {
Callable<Object> callableMock = mock(Callable.class);
when(connectionMock.exists(KEY_BYTES)).thenReturn(true);
when(connectionMock.get(KEY_BYTES)).thenReturn(null).thenReturn(VALUE_BYTES);
when(connectionMock.get(KEY_BYTES)).thenReturn(VALUE_BYTES);
assertThat((String) cache.get(KEY, callableMock), equalTo(VALUE));
verifyZeroInteractions(callableMock);
@@ -313,7 +313,7 @@ public class RedisCacheUnitTests {
}
});
verify(clusterConnectionMock, times(2)).get(eq(KEY_BYTES));
verify(clusterConnectionMock, times(1)).get(eq(KEY_BYTES));
verify(clusterConnectionMock, times(1)).set(eq(KEY_BYTES), eq(VALUE_BYTES));
verify(clusterConnectionMock, never()).multi();

View File

@@ -16,9 +16,11 @@
package org.springframework.data.redis.serializer;
import static org.hamcrest.core.Is.*;
import static org.hamcrest.core.IsInstanceOf.*;
import static org.hamcrest.core.IsNull.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.util.ReflectionTestUtils.*;
import static org.springframework.util.ObjectUtils.*;
@@ -26,6 +28,8 @@ import static org.springframework.util.ObjectUtils.*;
import java.io.IOException;
import org.junit.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.support.NullValue;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerationException;
@@ -152,6 +156,23 @@ public class GenericJackson2JsonRedisSerializerUnitTests {
new GenericJackson2JsonRedisSerializer(objectMapperMock).deserialize(new byte[] { 1 });
}
/**
* @see DATAREDIS-553
*/
@Test
public void shouldSerializeNullValueSoThatItCanBeDeserializedWithDefaultTypingEnabled() {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
NullValue nv = BeanUtils.instantiateClass(NullValue.class);
byte[] serializedValue = serializer.serialize(nv);
assertThat(serializedValue, is(notNullValue()));
Object deserializedValue = serializer.deserialize(serializedValue);
assertThat(deserializedValue, is(instanceOf(NullValue.class)));
}
private TypeResolverBuilder<?> extractTypeResolver(GenericJackson2JsonRedisSerializer serializer) {
ObjectMapper mapper = (ObjectMapper) getField(serializer, "mapper");