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

@@ -23,7 +23,8 @@ import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.Callable;
import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.cache.support.NullValue;
import org.springframework.cache.support.SimpleValueWrapper;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.RedisSystemException;
@@ -33,6 +34,10 @@ import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ClassUtils;
@@ -47,7 +52,7 @@ import org.springframework.util.ObjectUtils;
* @author Mark Paluch
*/
@SuppressWarnings("unchecked")
public class RedisCache implements Cache {
public class RedisCache extends AbstractValueAdaptingCache {
@SuppressWarnings("rawtypes")//
private final RedisOperations redisOperations;
@@ -64,13 +69,46 @@ public class RedisCache implements Cache {
*/
public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
long expiration) {
this(name, prefix, redisOperations, expiration, false);
}
/**
* Constructs a new <code>RedisCache</code> instance.
*
* @param name cache name
* @param prefix
* @param redisOperations
* @param expiration
* @param allowNullValues
* @since 1.8
*/
public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
long expiration, boolean allowNullValues) {
super(allowNullValues);
hasText(name, "non-empty cache name is required");
this.cacheMetadata = new RedisCacheMetadata(name, prefix);
this.cacheMetadata.setDefaultExpiration(expiration);
this.redisOperations = redisOperations;
this.cacheValueAccessor = new CacheValueAccessor(redisOperations.getValueSerializer());
RedisSerializer<?> serializer = redisOperations.getValueSerializer() != null ? redisOperations.getValueSerializer()
: (RedisSerializer<?>) new JdkSerializationRedisSerializer();
this.cacheValueAccessor = new CacheValueAccessor(serializer);
if (allowNullValues) {
if (redisOperations.getValueSerializer() instanceof StringRedisSerializer
|| redisOperations.getValueSerializer() instanceof GenericToStringSerializer
|| redisOperations.getValueSerializer() instanceof JacksonJsonRedisSerializer
|| redisOperations.getValueSerializer() instanceof Jackson2JsonRedisSerializer) {
throw new IllegalArgumentException(String.format(
"Redis does not allow keys with null value ¯\\_(ツ)_/¯. The chosen %s does not support generic type handling and therefore cannot be used with allowNullValues enabled. Please use a different RedisSerializer or disable null value support.",
ClassUtils.getShortName(redisOperations.getValueSerializer().getClass())));
}
}
}
/**
@@ -135,16 +173,19 @@ public class RedisCache implements Cache {
notNull(cacheKey, "CacheKey must not be null!");
byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(new BinaryRedisCacheElement(
new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
return connection.get(element.getKeyBytes());
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
return (bytes == null ? null : new RedisCacheElement(cacheKey, cacheValueAccessor.deserializeIfNecessary(bytes)));
if (!exists.booleanValue()) {
return null;
}
return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
}
/*
@@ -154,8 +195,24 @@ public class RedisCache implements Cache {
@Override
public void put(final Object key, final Object value) {
put(new RedisCacheElement(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix()).withKeySerializer(
redisOperations.getKeySerializer()), value).expireAfter(cacheMetadata.getDefaultExpiration()));
put(new RedisCacheElement(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix())
.withKeySerializer(redisOperations.getKeySerializer()), toStoreValue(value))
.expireAfter(cacheMetadata.getDefaultExpiration()));
}
/*
* (non-Javadoc)
* @see org.springframework.cache.support.AbstractValueAdaptingCache#fromStoreValue(java.lang.Object)
*/
@Override
protected Object fromStoreValue(Object storeValue) {
// we need this override for the GenericJackson2JsonRedisSerializer support.
if (isAllowNullValues() && storeValue instanceof NullValue) {
return null;
}
return super.fromStoreValue(storeValue);
}
/**
@@ -181,8 +238,8 @@ public class RedisCache implements Cache {
public ValueWrapper putIfAbsent(Object key, final Object value) {
return putIfAbsent(new RedisCacheElement(new RedisCacheKey(key).usePrefix(cacheMetadata.getKeyPrefix())
.withKeySerializer(redisOperations.getKeySerializer()), value)
.expireAfter(cacheMetadata.getDefaultExpiration()));
.withKeySerializer(redisOperations.getKeySerializer()), toStoreValue(value))
.expireAfter(cacheMetadata.getDefaultExpiration()));
}
/**
@@ -253,6 +310,29 @@ public class RedisCache implements Cache {
return (value != null ? new SimpleValueWrapper(value) : null);
}
/*
* (non-Javadoc)
* @see org.springframework.cache.support.AbstractValueAdaptingCache#lookup(java.lang.Object)
*/
@Override
protected Object lookup(Object key) {
RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key
: new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix())
.withKeySerializer(redisOperations.getKeySerializer());
byte[] bytes = (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(
new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
@Override
public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
return connection.get(element.getKeyBytes());
}
});
return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes);
}
/**
* Metadata required to maintain {@link RedisCache}. Keeps track of additional data structures required for processing
* cache operations.

View File

@@ -29,12 +29,14 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.support.NullValue;
import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -70,6 +72,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
private Set<String> configuredCacheNames;
private final boolean cacheNullValues;
/**
* Construct a {@link RedisCacheManager}.
*
@@ -89,7 +93,25 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
this(redisOperations, cacheNames, false);
}
/**
* Construct a static {@link RedisCacheManager}, managing caches for the specified cache names only. <br />
* <br />
* <strong>NOTE</strong> When enabling {@code cacheNullValues} please make sure the {@link RedisSerializer} used by
* {@link RedisOperations} is capable of serializing {@link NullValue}.
*
* @param redisOperations {@link RedisOperations} to work upon.
* @param cacheNames {@link Collection} of known cache names.
* @param cacheNullValues set to {@literal true} to allow caching {@literal null}.
* @since 1.8
*/
@SuppressWarnings("rawtypes")
public RedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean cacheNullValues) {
this.redisOperations = redisOperations;
this.cacheNullValues = cacheNullValues;
setCacheNames(cacheNames);
}
@@ -235,7 +257,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
long expiration = computeExpiration(cacheName);
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration);
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
cacheNullValues);
}
protected long computeExpiration(String name) {

View File

@@ -15,15 +15,22 @@
*/
package org.springframework.data.redis.serializer;
import java.io.IOException;
import org.springframework.cache.support.NullValue;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* @author Christoph Strobl
@@ -51,6 +58,10 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
this(new ObjectMapper());
// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
} else {
@@ -119,4 +130,40 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex);
}
}
/**
* {@link StdSerializer} adding class information required by default typing. This allows de-/seriialization of
* {@link NullValue}.
*
* @author Christoph Strobl
* @since 1.8
*/
private class NullValueSerializer extends StdSerializer<NullValue> {
private static final long serialVersionUID = 1999052150548658808L;
private final String classIdentifyer;
/**
* @param classIdentifyer can be {@literal null} and will be defaulted to {@code @class}.
*/
NullValueSerializer(String classIdentifyer) {
super(NullValue.class);
this.classIdentifyer = StringUtils.hasText(classIdentifyer) ? classIdentifyer : "@class";
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField(classIdentifyer, NullValue.class.getName());
jgen.writeEndObject();
}
}
}