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 Jun 16, 2015
1 parent 5111d05 commit 20f2048
Show file tree
Hide file tree
Showing 3 changed files with 360 additions and 3 deletions.
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 20f2048

Please sign in to comment.