From 2e7a04fcd087c27dc720503da80d64dd55dc45c5 Mon Sep 17 00:00:00 2001 From: Jennifer Hickey Date: Wed, 24 Jul 2013 16:30:17 -0700 Subject: [PATCH] Add deserialize of exec results to RedisTemplate DATAREDIS-228 - RedisTemplate.exec() will now deserialize raw results using RT serializers, where applicable. This behavior is disabled if convertPipelineAndTxResults is set to false on the ConnectionFactory - Add new RedisTemplate.exec(RedisSerializer) method that will deserialize all results using the provided serializer. --- .../connection/RedisConnectionFactory.java | 13 +++ .../jredis/JredisConnectionFactory.java | 7 ++ .../data/redis/core/RedisOperations.java | 13 +++ .../data/redis/core/RedisTemplate.java | 82 +++++++++++++- .../redis/serializer/SerializationUtils.java | 13 +++ .../redis/core/StringRedisTemplateTests.java | 104 ++++++++++++++++++ 6 files changed, 230 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java index 1c1ea49e3..1f3136abc 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -31,4 +31,17 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * @return connection for interacting with Redis. */ RedisConnection getConnection(); + + /** + * Specifies if pipelined results should be converted to the expected data + * type. If false, results of {@link RedisConnection#closePipeline()} and {RedisConnection#exec()} + * will be of the type returned by the underlying driver + * + * This method is mostly for backwards compatibility with 1.0. It is generally + * always a good idea to allow results to be converted and deserialized. + * In fact, this is now the default behavior. + * + * @return Whether or not to convert pipeline and tx results + */ + boolean getConvertPipelineAndTxResults(); } diff --git a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java index 6ee83698e..873266c5d 100644 --- a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnectionFactory.java @@ -201,4 +201,11 @@ public class JredisConnectionFactory implements InitializingBean, DisposableBean Assert.isTrue(index >= 0, "invalid DB index (a positive index required)"); this.dbIndex = index; } + + /** + * {@link JredisConnection} does not support pipeline or transactions + */ + public boolean getConvertPipelineAndTxResults() { + return false; + } } \ No newline at end of file diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index f162cf5d4..901b621c7 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -120,6 +120,19 @@ public interface RedisOperations { List exec(); + /** + * Execute a transaction, using the provided {@link RedisSerializer} to deserialize + * any results that are byte[]s or Collections of byte[]s. If a result is a Map, the + * provided {@link RedisSerializer} will be used for both the keys and values. Other result + * types (Long, Boolean, etc) are left as-is in the converted results. Tuple results are + * automatically converted to TypedTuples. + * + * @param valueSerializer The {@link RedisSerializer} to use for deserializing the results + * of transaction exec + * @return The deserialized results of transaction exec + */ + List exec(RedisSerializer valueSerializer); + // pubsub functionality on the template void convertAndSend(String destination, Object message); diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 450c5991e..43d7d05b0 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -20,15 +20,20 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.RedisSystemException; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.SortParameters; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; import org.springframework.data.redis.core.query.QueryUtils; import org.springframework.data.redis.core.query.SortQuery; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; @@ -414,14 +419,87 @@ public class RedisTemplate extends RedisAccessor implements RedisOperation return (K) keySerializer.deserialize(value); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private List deserializeMixedResults(List rawValues, RedisSerializer valueSerializer, + RedisSerializer hashKeySerializer, RedisSerializer hashValueSerializer) { + if(rawValues == null) { + return null; + } + List values = new ArrayList(); + for(Object rawValue: rawValues) { + if(rawValue instanceof byte[]) { + values.add(valueSerializer.deserialize((byte[])rawValue)); + } else if(rawValue instanceof List) { + // Lists are the only potential Collections of mixed values.... + values.add(deserializeMixedResults((List)rawValue, valueSerializer, hashKeySerializer, hashValueSerializer)); + } else if(rawValue instanceof Set && !(((Set)rawValue).isEmpty())) { + values.add(deserializeSet((Set)rawValue, valueSerializer)); + } else if(rawValue instanceof Map && !(((Map)rawValue).isEmpty()) && + ((Map)rawValue).values().iterator().next() instanceof byte[]) { + values.add(SerializationUtils.deserialize((Map)rawValue, hashKeySerializer, hashValueSerializer)); + } else { + values.add(rawValue); + } + } + return values; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Set deserializeSet(Set rawSet, RedisSerializer valueSerializer) { + if(rawSet.isEmpty()) { + return rawSet; + } + Object setValue = rawSet.iterator().next(); + if(setValue instanceof byte[]) { + return (SerializationUtils.deserialize((Set)rawSet, valueSerializer)); + }else if(setValue instanceof Tuple) { + return deserializeTupleValues(rawSet, valueSerializer); + } else { + return rawSet; + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Set> deserializeTupleValues(Set rawValues, RedisSerializer valueSerializer) { + Set> set = new LinkedHashSet>(rawValues.size()); + for (Tuple rawValue : rawValues) { + set.add(new DefaultTypedTuple(valueSerializer.deserialize(rawValue.getValue()), rawValue.getScore())); + } + return set; + } + // // RedisOperations // + /** + * Execute a transaction, using the default {@link RedisSerializer}s to deserialize + * any results that are byte[]s or Collections or Maps of byte[]s or Tuples. Other result + * types (Long, Boolean, etc) are left as-is in the converted results. + * + * If conversion of tx results has been disabled in the {@link ConnectionFactory}, + * the results of exec will be returned without deserialization. This check is mostly for + * backwards compatibility with 1.0. + * + * @return The (possibly deserialized) results of transaction exec + */ public List exec() { + List results = execRaw(); + if(getConnectionFactory().getConvertPipelineAndTxResults()) { + return deserializeMixedResults(results, valueSerializer, + hashKeySerializer, hashValueSerializer); + } else { + return results; + } + } + + public List exec(RedisSerializer valueSerializer) { + return deserializeMixedResults(execRaw(), valueSerializer, valueSerializer, + valueSerializer); + } + + protected List execRaw() { return execute(new RedisCallback>() { - - public List doInRedis(RedisConnection connection) throws DataAccessException { return connection.exec(); } diff --git a/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java b/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java index b922ec0c4..37427310a 100644 --- a/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java +++ b/src/main/java/org/springframework/data/redis/serializer/SerializationUtils.java @@ -80,4 +80,17 @@ public abstract class SerializationUtils { } return ret; } + + public static Map deserialize(Map rawValues, + RedisSerializer hashKeySerializer, RedisSerializer hashValueSerializer) { + if (rawValues == null) { + return null; + } + Map map = new LinkedHashMap(rawValues.size()); + for (Map.Entry entry : rawValues.entrySet()) { + map.put((HK) hashKeySerializer.deserialize(entry.getKey()), + (HV) hashValueSerializer.deserialize(entry.getValue())); + } + return map; + } } \ No newline at end of file diff --git a/src/test/java/org/springframework/data/redis/core/StringRedisTemplateTests.java b/src/test/java/org/springframework/data/redis/core/StringRedisTemplateTests.java index 3b310d747..5cdb7c6b2 100644 --- a/src/test/java/org/springframework/data/redis/core/StringRedisTemplateTests.java +++ b/src/test/java/org/springframework/data/redis/core/StringRedisTemplateTests.java @@ -19,16 +19,29 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.data.redis.SpinBarrier.waitFor; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; import org.springframework.data.redis.RedisTestProfileValueSource; +import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.TestCondition; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.StringRedisConnection; +import org.springframework.data.redis.connection.srp.SrpConnectionFactory; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.annotation.ProfileValueSourceConfiguration; import org.springframework.test.context.ContextConfiguration; @@ -111,4 +124,95 @@ public class StringRedisTemplateTests { }); assertEquals(value,"it"); } + + @Test + public void testExecDeserializes() { + List results = redisTemplate.execute(new SessionCallback>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public List execute(RedisOperations operations) throws DataAccessException { + operations.multi(); + operations.opsForValue().set("foo", "bar"); + // byte[] + operations.opsForValue().get("foo"); + operations.opsForList().leftPush("foolist", "something"); + // List + operations.opsForList().range("foolist", 0l, 1l); + operations.opsForSet().add("fooset", "a"); + // Set + operations.opsForSet().members("fooset"); + operations.opsForZSet().add("foozset", "Joe", 1d); + // Set + operations.opsForZSet().rangeWithScores("foozset", 0l, -1l); + operations.opsForHash().put("foomap", "test", "passed"); + // Map + operations.opsForHash().entries("foomap"); + return operations.exec(); + } + }); + List list = Collections.singletonList("something"); + Set stringSet = new HashSet(Collections.singletonList("a")); + Set> tupleSet = new LinkedHashSet>( + Collections.singletonList(new DefaultTypedTuple("Joe", 1d))); + Map map = new LinkedHashMap(); + map.put("test", "passed"); + assertEquals(Arrays.asList(new Object[] {"bar", 1l, list, true, stringSet, true, tupleSet, true, map}), + results); + } + + @Test + public void testExecCustomSerializer() { + List results = redisTemplate.execute(new SessionCallback>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public List execute(RedisOperations operations) throws DataAccessException { + operations.multi(); + operations.opsForValue().set("foo", "5"); + // byte[] + operations.opsForValue().get("foo"); + operations.opsForList().leftPush("foolist", "6"); + // List + operations.opsForList().range("foolist", 0l, 1l); + operations.opsForSet().add("fooset", "7"); + // Set + operations.opsForSet().members("fooset"); + operations.opsForZSet().add("foozset", "9", 1d); + // Set + operations.opsForZSet().rangeWithScores("foozset", 0l, -1l); + operations.opsForZSet().range("foozset", 0, -1); + operations.opsForHash().put("foomap", "10", "11"); + // Map + operations.opsForHash().entries("foomap"); + return operations.exec(new GenericToStringSerializer(Long.class)); + } + }); + // Everything should be converted to Longs + List list = Collections.singletonList(6l); + Set longSet = new HashSet(Collections.singletonList(7l)); + Set> tupleSet = new LinkedHashSet>( + Collections.singletonList(new DefaultTypedTuple(9l, 1d))); + Set zSet = new LinkedHashSet(Collections.singletonList(9l)); + Map map = new LinkedHashMap(); + map.put(10l, 11l); + assertEquals(Arrays.asList(new Object[] {5l, 1l, list, true, longSet, true, tupleSet, zSet, true, map}), + results); + } + + @Test + public void testExecConversionDisabled() { + SrpConnectionFactory factory2 = new SrpConnectionFactory(SettingsUtils.getHost(), SettingsUtils.getPort()); + factory2.setConvertPipelineAndTxResults(false); + factory2.afterPropertiesSet(); + StringRedisTemplate template = new StringRedisTemplate(factory2); + template.afterPropertiesSet(); + List results = template.execute(new SessionCallback>() { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public List execute(RedisOperations operations) throws DataAccessException { + operations.multi(); + operations.opsForValue().set("foo","bar"); + operations.opsForValue().get("foo"); + return operations.exec(); + } + }); + // first value is "OK" from set call, results should still be in byte[] + assertEquals("bar", new String((byte[])results.get(1))); + } }