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:
committed by
Mark Paluch
parent
72d6df00c8
commit
4c54bf972a
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user