-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DATAREDIS-392 - Add Gerneric Jackson2 RedisSerializer.
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
1 parent
5111d05
commit 20f2048
Showing
3 changed files
with
360 additions
and
3 deletions.
There are no files selected for viewing
122 changes: 122 additions & 0 deletions
122
...in/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 228 additions & 0 deletions
228
...rg/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
|
||
} |