-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DATAREDIS-390 - Improve Support for JSON Serialization.
Introduced GenericJackson2RedisSerializer that is capable of serialising arbitrary java objects into redis without a priority knowledge of the used types. This is achieved by encoding the actual type information with the value. Previously users had to use Jackson2RedisSerializer which only supported serialising one type. Original pull request: #136.
- Loading branch information
Thomas Darimont
committed
Apr 7, 2015
1 parent
90233d8
commit d3c8821
Showing
4 changed files
with
297 additions
and
1 deletion.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
...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,153 @@ | ||
/* | ||
* 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 java.nio.ByteBuffer; | ||
import java.nio.charset.Charset; | ||
|
||
import org.springframework.util.Assert; | ||
import org.springframework.util.ClassUtils; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.ser.SerializerFactory; | ||
|
||
/** | ||
* {@link RedisSerializer} that can read and write JSON for arbitrary input types at the expense of additional type | ||
* information stored with with value by using <a href="https://github.com/FasterXML/jackson-core">Jackson's</a> and <a | ||
* href="https://github.com/FasterXML/jackson-databind">Jackson Databind</a> {@link ObjectMapper}. | ||
* <p> | ||
* This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. | ||
* <b>Note:</b>Null objects are serialized as empty arrays and vice versa. | ||
* <p> | ||
* The serialized form consists of: | ||
* <ol> | ||
* <li>Payload type name length (int, 4 bytes)</li> | ||
* <li>Payload type name in bytes encoded with UTF-8</li> | ||
* <li>Payload data bytes</li> | ||
* </ol> | ||
* | ||
* @author Thomas Darimont | ||
* @since 1.6 | ||
*/ | ||
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> { | ||
|
||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); | ||
|
||
private ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
/** | ||
* Creates a new {@link GenericJackson2JsonRedisSerializer}. | ||
*/ | ||
public GenericJackson2JsonRedisSerializer() {} | ||
|
||
/* (non-Javadoc) | ||
* @see org.springframework.data.redis.serializer.RedisSerializer#deserialize(byte[]) | ||
*/ | ||
@Override | ||
public Object deserialize(byte[] bytes) throws SerializationException { | ||
|
||
if (SerializationUtils.isEmpty(bytes)) { | ||
return null; | ||
} | ||
|
||
try { | ||
|
||
ByteBuffer buffer = ByteBuffer.wrap(bytes); | ||
|
||
int typeLength = buffer.getInt(); | ||
byte[] typeBytes = new byte[typeLength]; | ||
buffer.get(typeBytes); | ||
|
||
// resolve valuetype first to allow early exit on unresolveable types | ||
Class<?> valueType = resolveTypeFrom(typeBytes); | ||
|
||
byte[] valueBytes = new byte[buffer.remaining()]; | ||
buffer.get(valueBytes); | ||
|
||
return this.objectMapper.readValue(valueBytes, 0, valueBytes.length, valueType); | ||
} catch (Exception ex) { | ||
throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); | ||
} | ||
} | ||
|
||
/** | ||
* Resolves the {@link Class} to be used from the given {@code typeBytes}. | ||
* <p> | ||
* This can be overridden to cache type lookups. | ||
* | ||
* @param typeBytes | ||
* @return | ||
* @throws ClassNotFoundException | ||
*/ | ||
protected Class<?> resolveTypeFrom(byte[] typeBytes) throws ClassNotFoundException { | ||
|
||
String className = new String(typeBytes, DEFAULT_CHARSET); | ||
return ClassUtils.resolveClassName(className, getClass().getClassLoader()); | ||
} | ||
|
||
/* (non-Javadoc) | ||
* @see org.springframework.data.redis.serializer.RedisSerializer#serialize(java.lang.Object) | ||
*/ | ||
@Override | ||
public byte[] serialize(Object value) throws SerializationException { | ||
|
||
if (value == null) { | ||
return SerializationUtils.EMPTY_ARRAY; | ||
} | ||
|
||
try { | ||
|
||
byte[] valueBytes = this.objectMapper.writeValueAsBytes(value); | ||
byte[] typeBytes = convertTypeToBytes(value.getClass()); | ||
|
||
ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + typeBytes.length + valueBytes.length); | ||
buffer.putInt(typeBytes.length); | ||
buffer.put(typeBytes); | ||
buffer.put(valueBytes); | ||
|
||
return buffer.array(); | ||
} catch (Exception ex) { | ||
throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); | ||
} | ||
} | ||
|
||
/** | ||
* Converts a given {@code Class} type to a {@code byte[]} representation. | ||
* <p> | ||
* Note that the representation must be readable from {@link #resolveTypeFrom(byte[])}. | ||
* | ||
* @param type | ||
* @return | ||
*/ | ||
protected byte[] convertTypeToBytes(Class<?> type) { | ||
return type.getName().getBytes(DEFAULT_CHARSET); | ||
} | ||
|
||
/** | ||
* Sets the {@code ObjectMapper} for this view. If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} | ||
* is used. | ||
* <p> | ||
* Setting a custom-configured {@code 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. The other option for refining the serialization process is to use Jackson's provided annotations on | ||
* the types to be serialized, in which case a custom-configured ObjectMapper is unnecessary. | ||
*/ | ||
public void setObjectMapper(ObjectMapper objectMapper) { | ||
|
||
Assert.notNull(objectMapper, "'objectMapper' must not be null"); | ||
this.objectMapper = objectMapper; | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/test/java/org/springframework/data/redis/AddressObjectFactory.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,32 @@ | ||
/* | ||
* 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; | ||
|
||
import java.util.concurrent.atomic.AtomicInteger; | ||
|
||
/** | ||
* @author Thomas Darimont | ||
*/ | ||
public class AddressObjectFactory implements ObjectFactory<Address> { | ||
|
||
private final AtomicInteger counter = new AtomicInteger(); | ||
|
||
@Override | ||
public Address instance() { | ||
return new Address("Street " + counter.intValue(), counter.intValue()); | ||
} | ||
|
||
} |
87 changes: 87 additions & 0 deletions
87
...va/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerTests.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,87 @@ | ||
/* | ||
* 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.junit.Assert.*; | ||
|
||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
import org.hamcrest.core.Is; | ||
import org.hamcrest.core.IsNull; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.springframework.data.redis.AddressObjectFactory; | ||
import org.springframework.data.redis.ObjectFactory; | ||
import org.springframework.data.redis.Person; | ||
import org.springframework.data.redis.PersonObjectFactory; | ||
|
||
/** | ||
* @author Thomas Darimont | ||
*/ | ||
public class GenericJackson2JsonRedisSerializerTests { | ||
|
||
private GenericJackson2JsonRedisSerializer serializer; | ||
|
||
private List<ObjectFactory<? extends Object>> objectFactories; | ||
|
||
@Before | ||
@SuppressWarnings("unchecked") | ||
public void setUp() { | ||
|
||
this.serializer = new GenericJackson2JsonRedisSerializer(); | ||
this.objectFactories = Arrays.<ObjectFactory<? extends Object>> asList(new PersonObjectFactory(), | ||
new AddressObjectFactory()); | ||
} | ||
|
||
/** | ||
* @see DATAREDIS-390 | ||
*/ | ||
@Test | ||
public void beAbleToSerializeMultipleTypes() throws Exception { | ||
|
||
for (ObjectFactory<? extends Object> factory : objectFactories) { | ||
Object instance = factory.instance(); | ||
assertEquals(instance, serializer.deserialize(serializer.serialize(instance))); | ||
} | ||
} | ||
|
||
@Test | ||
public void testJackson2JsonSerializerShouldReturnEmptyByteArrayWhenSerializingNull() { | ||
assertThat(serializer.serialize(null), Is.is(new byte[0])); | ||
} | ||
|
||
@Test | ||
public void testJackson2JsonSerializerShouldReturnNullWhenDerserializingEmtyByteArray() { | ||
assertThat(serializer.deserialize(new byte[0]), IsNull.nullValue()); | ||
} | ||
|
||
@Test(expected = SerializationException.class) | ||
public void testJackson2JsonSerilizerShouldThrowExceptionWhenDeserializingInvalidByteArray() { | ||
|
||
Person person = new PersonObjectFactory().instance(); | ||
byte[] serializedValue = serializer.serialize(person); | ||
Arrays.sort(serializedValue); // corrupt serialization result | ||
|
||
serializer.deserialize(serializedValue); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testJackson2JsonSerilizerThrowsExceptionWhenSettingNullObjectMapper() { | ||
serializer.setObjectMapper(null); | ||
} | ||
|
||
} |
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