Skip to content

Commit

Permalink
DATAREDIS-392 - Add Gerneric Jackson2 RedisSerializer.
Browse files Browse the repository at this point in the history
We introduced GenericJackson2JsonRedisSerializer holding a preconfigured ObjectMapper which writes Class type informations into the JSON structure. This enables polymorphic deserialization and allows RedisCache manager to operate upon a RedisTemplate storing data in JSON format, which had until this only been possible using the Jdk Serializer.
  • Loading branch information
christophstrobl committed Aug 28, 2015
1 parent c3de444 commit a1b4ab4
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/asciidoc/reference/redis.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Object> {

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> T deserialize(byte[] source, Class<T> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -27,6 +27,7 @@
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;
Expand Down Expand Up @@ -115,6 +116,12 @@ public static Collection<Object[]> testParams() {
jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer);
jackson2JsonPersonTemplate.afterPropertiesSet();

GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer();
RedisTemplate<String, Person> genericJackson2JsonPersonTemplate = new RedisTemplate<String, Person>();
genericJackson2JsonPersonTemplate.setConnectionFactory(jedisConnectionFactory);
genericJackson2JsonPersonTemplate.setValueSerializer(genericJackson2JsonSerializer);
genericJackson2JsonPersonTemplate.afterPropertiesSet();

return Arrays.asList(new Object[][] { //
{ stringTemplate, stringFactory, stringFactory }, //
{ longTemplate, stringFactory, longFactory }, //
Expand All @@ -124,7 +131,7 @@ public static Collection<Object[]> testParams() {
{ xstreamStringTemplate, stringFactory, stringFactory }, //
{ xstreamPersonTemplate, stringFactory, personFactory }, //
{ jsonPersonTemplate, stringFactory, personFactory }, //
{ jackson2JsonPersonTemplate, stringFactory, personFactory } //
});
{ jackson2JsonPersonTemplate, stringFactory, personFactory }, //
{ genericJackson2JsonPersonTemplate, stringFactory, personFactory } });
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}

}

0 comments on commit a1b4ab4

Please sign in to comment.