diff --git a/src/asciidoc/reference/redis.adoc b/src/asciidoc/reference/redis.adoc index 6c7deca6c..8f3b4e8fc 100644 --- a/src/asciidoc/reference/redis.adoc +++ b/src/asciidoc/reference/redis.adoc @@ -344,7 +344,7 @@ public void useCallback() { [[redis:serializer]] == Serializers -From the framework perspective, the data stored in Redis is just bytes. While Redis itself supports various types, for the most part these refer to the way the data is stored rather then what it represents. It is up to the user to decide whether the information gets translated into Strings or any other objects. The conversion between the user (custom) types and raw data (and vice-versa) is handled in Spring Data Redis through the `RedisSerializer` interface (package `org.springframework.data.redis.serializer`) which as the name implies, takes care of the serialization process. Multiple implementations are available out of the box, two of which have been already mentioned before in this documentation: the `StringRedisSerializer` and the `JdkSerializationRedisSerializer`. However one can use `OxmSerializer` for Object/XML mapping through Spring 3 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[OXM] support or either `JacksonJsonRedisSerializer` or `Jackson2JsonRedisSerializer` for storing data in http://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values - it can be used for keys, values or hashes without any restrictions. +From the framework perspective, the data stored in Redis is just bytes. While Redis itself supports various types, for the most part these refer to the way the data is stored rather then what it represents. It is up to the user to decide whether the information gets translated into Strings or any other objects. The conversion between the user (custom) types and raw data (and vice-versa) is handled in Spring Data Redis through the `RedisSerializer` interface (package `org.springframework.data.redis.serializer`) which as the name implies, takes care of the serialization process. Multiple implementations are available out of the box, two of which have been already mentioned before in this documentation: the `StringRedisSerializer` and the `JdkSerializationRedisSerializer`. However one can use `OxmSerializer` for Object/XML mapping through Spring 3 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html[OXM] support or either `JacksonJsonRedisSerializer`, `Jackson2JsonRedisSerializer` or `GenericJackson2JsonRedisSerializer for storing data in http://en.wikipedia.org/wiki/JSON[JSON] format. Do note that the storage format is not limited only to values - it can be used for keys, values or hashes without any restrictions. :leveloffset: 2 include::{referenceDir}/redis-messaging.adoc[] diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java new file mode 100644 index 000000000..07f10b1ae --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +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.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.ser.SerializerFactory; + +/** + * @author Christoph Strobl + * @since 1.6 + */ +public class GenericJackson2JsonRedisSerializer implements RedisSerializer { + + private final ObjectMapper mapper; + + /** + * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing. + */ + public GenericJackson2JsonRedisSerializer() { + this((String) null); + } + + /** + * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing using the + * given {@literal name}. In case of an {@literal empty} or {@literal null} String the default + * {@link JsonTypeInfo.Id#CLASS} will be used. + * + * @param classPropertyTypeName Name of the JSON property holding type information. Can be {@literal null}. + */ + public GenericJackson2JsonRedisSerializer(String classPropertyTypeName) { + + this(new ObjectMapper()); + + if (StringUtils.hasText(classPropertyTypeName)) { + mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName); + } else { + mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY); + } + } + + /** + * Setting a custom-configured {@link ObjectMapper} is one way to take further control of the JSON serialization + * process. For example, an extended {@link SerializerFactory} can be configured that provides custom serializers for + * specific types. + * + * @param mapper must not be {@literal null}. + */ + public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + this.mapper = mapper; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisSerializer#serialize(java.lang.Object) + */ + @Override + public byte[] serialize(Object source) throws SerializationException { + + if (source == null) { + return SerializationUtils.EMPTY_ARRAY; + } + + try { + return mapper.writeValueAsBytes(source); + } catch (JsonProcessingException e) { + throw new SerializationException("Could not write JSON: " + e.getMessage(), e); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisSerializer#deserialize(byte[]) + */ + @Override + public Object deserialize(byte[] source) throws SerializationException { + return deserialize(source, Object.class); + } + + /** + * @param source can be {@literal null}. + * @param type must not be {@literal null}. + * @return {@literal null} for empty source. + * @throws SerializationException + */ + public T deserialize(byte[] source, Class type) throws SerializationException { + + Assert.notNull(type, + "Deserialization type must not be null! Pleaes provide Object.class to make use of Jackson2 default typing."); + + if (SerializationUtils.isEmpty(source)) { + return null; + } + + try { + return mapper.readValue(source, type); + } catch (Exception ex) { + throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); + } + } +} diff --git a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java index a6369c4f9..1e7a839bf 100644 --- a/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java +++ b/src/test/java/org/springframework/data/redis/core/AbstractOperationsTestParams.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 - 2014 the original author or authors. + * Copyright 2013 - 2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.GenericToStringSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer; @@ -115,6 +116,12 @@ abstract public class AbstractOperationsTestParams { jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer); jackson2JsonPersonTemplate.afterPropertiesSet(); + GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer(); + RedisTemplate genericJackson2JsonPersonTemplate = new RedisTemplate(); + genericJackson2JsonPersonTemplate.setConnectionFactory(jedisConnectionFactory); + genericJackson2JsonPersonTemplate.setValueSerializer(genericJackson2JsonSerializer); + genericJackson2JsonPersonTemplate.afterPropertiesSet(); + return Arrays.asList(new Object[][] { // { stringTemplate, stringFactory, stringFactory }, // { longTemplate, stringFactory, longFactory }, // @@ -124,7 +131,7 @@ abstract public class AbstractOperationsTestParams { { xstreamStringTemplate, stringFactory, stringFactory }, // { xstreamPersonTemplate, stringFactory, personFactory }, // { jsonPersonTemplate, stringFactory, personFactory }, // - { jackson2JsonPersonTemplate, stringFactory, personFactory } // - }); + { jackson2JsonPersonTemplate, stringFactory, personFactory }, // + { genericJackson2JsonPersonTemplate, stringFactory, personFactory } }); } } diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java new file mode 100644 index 000000000..e61651d06 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -0,0 +1,228 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.serializer; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.util.ReflectionTestUtils.*; +import static org.springframework.util.ObjectUtils.*; + +import java.io.IOException; + +import org.junit.Test; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; +import com.fasterxml.jackson.databind.type.TypeFactory; + +/** + * @author Christoph Strobl + */ +public class GenericJackson2JsonRedisSerializerUnitTests { + + private static final SimpleObject SIMPLE_OBJECT = new SimpleObject(1L); + private static final ComplexObject COMPLEX_OBJECT = new ComplexObject("steelheart", SIMPLE_OBJECT); + + /** + * @see DATAREDIS-392 + */ + @Test + public void shouldUseDefaultTyping() { + assertThat(extractTypeResolver(new GenericJackson2JsonRedisSerializer()), notNullValue()); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void shouldUseDefaultTypingWhenClassPropertyNameIsEmpty() { + + TypeResolverBuilder typeResolver = extractTypeResolver(new GenericJackson2JsonRedisSerializer("")); + assertThat((String) getField(typeResolver, "_typeProperty"), is(JsonTypeInfo.Id.CLASS.getDefaultPropertyName())); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void shouldUseDefaultTypingWhenClassPropertyNameIsNull() { + + TypeResolverBuilder typeResolver = extractTypeResolver(new GenericJackson2JsonRedisSerializer((String) null)); + assertThat((String) getField(typeResolver, "_typeProperty"), is(JsonTypeInfo.Id.CLASS.getDefaultPropertyName())); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void shouldUseDefaultTypingWhenClassPropertyNameIsProvided() { + + TypeResolverBuilder typeResolver = extractTypeResolver(new GenericJackson2JsonRedisSerializer("firefight")); + assertThat((String) getField(typeResolver, "_typeProperty"), is("firefight")); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void serializeShouldReturnEmptyByteArrayWhenSouceIsNull() { + assertThat(new GenericJackson2JsonRedisSerializer().serialize(null), is(SerializationUtils.EMPTY_ARRAY)); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void deserializeShouldReturnNullWhenSouceIsNull() { + assertThat(new GenericJackson2JsonRedisSerializer().deserialize(null), nullValue()); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void deserializeShouldReturnNullWhenSouceIsEmptyArray() { + assertThat(new GenericJackson2JsonRedisSerializer().deserialize(SerializationUtils.EMPTY_ARRAY), nullValue()); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void deserializeShouldBeAbleToRestoreSimpleObjectAfterSerialization() { + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + + assertThat((SimpleObject) serializer.deserialize(serializer.serialize(SIMPLE_OBJECT)), is(SIMPLE_OBJECT)); + } + + /** + * @see DATAREDIS-392 + */ + @Test + public void deserializeShouldBeAbleToRestoreComplexObjectAfterSerialization() { + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(); + + assertThat((ComplexObject) serializer.deserialize(serializer.serialize(COMPLEX_OBJECT)), is(COMPLEX_OBJECT)); + } + + /** + * @see DATAREDIS-392 + */ + @Test(expected = SerializationException.class) + public void serializeShouldThrowSerializationExceptionProcessingError() throws JsonProcessingException { + + ObjectMapper objectMapperMock = mock(ObjectMapper.class); + when(objectMapperMock.writeValueAsBytes(anyObject())).thenThrow(new JsonGenerationException("nightwielder")); + + new GenericJackson2JsonRedisSerializer(objectMapperMock).serialize(SIMPLE_OBJECT); + } + + /** + * @see DATAREDIS-392 + */ + @Test(expected = SerializationException.class) + public void deserializeShouldThrowSerializationExceptionProcessingError() throws IOException { + + ObjectMapper objectMapperMock = mock(ObjectMapper.class); + when(objectMapperMock.readValue(any(byte[].class), any(Class.class))) + .thenThrow(new JsonMappingException("conflux")); + + new GenericJackson2JsonRedisSerializer(objectMapperMock).deserialize(new byte[] { 1 }); + } + + private TypeResolverBuilder extractTypeResolver(GenericJackson2JsonRedisSerializer serializer) { + + ObjectMapper mapper = (ObjectMapper) getField(serializer, "mapper"); + return mapper.getSerializationConfig().getDefaultTyper(TypeFactory.defaultInstance().constructType(Object.class)); + } + + static class ComplexObject { + + public String stringValue; + public SimpleObject simpleObject; + + public ComplexObject() {} + + public ComplexObject(String stringValue, SimpleObject simpleObject) { + this.stringValue = stringValue; + this.simpleObject = simpleObject; + } + + @Override + public int hashCode() { + return nullSafeHashCode(stringValue) + nullSafeHashCode(simpleObject); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ComplexObject)) { + return false; + } + ComplexObject other = (ComplexObject) obj; + return nullSafeEquals(this.stringValue, other.stringValue) + && nullSafeEquals(this.simpleObject, other.simpleObject); + } + + } + + static class SimpleObject { + + public Long longValue; + + public SimpleObject() {} + + public SimpleObject(Long longValue) { + this.longValue = longValue; + } + + @Override + public int hashCode() { + return nullSafeHashCode(this.longValue); + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof SimpleObject)) { + return false; + } + SimpleObject other = (SimpleObject) obj; + return nullSafeEquals(this.longValue, other.longValue); + } + } + +}