Skip to content

Commit

Permalink
DATAREDIS-390 - Improve Support for JSON Serialization.
Browse files Browse the repository at this point in the history
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
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 1 deletion.
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;
}
}
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());
}

}
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);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2011-2013 the original author or authors.
* Copyright 2011-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 @@ -29,6 +29,7 @@

/**
* @author Jennifer Hickey
* @author Thomas Darimont
*/
public class SimpleRedisSerializerTests {

Expand Down Expand Up @@ -157,4 +158,27 @@ public void testJsonSerializer() throws Exception {
assertEquals(p1, serializer.deserialize(serializer.serialize(p1)));
}

@Test
public void testJackson2JsonSerializer() throws Exception {

Jackson2JsonRedisSerializer<Person> serializer = new Jackson2JsonRedisSerializer<Person>(Person.class);
String value = UUID.randomUUID().toString();
Person p1 = new Person(value, value, 1, new Address(value, 2));
assertEquals(p1, serializer.deserialize(serializer.serialize(p1)));
assertEquals(p1, serializer.deserialize(serializer.serialize(p1)));
}

/**
* @see DATAREDIS-390
*/
@Test
public void testGenericJackson2JsonSerializer() throws Exception {

GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
String value = UUID.randomUUID().toString();
Person p1 = new Person(value, value, 1, new Address(value, 2));
assertEquals(p1, serializer.deserialize(serializer.serialize(p1)));
assertEquals(p1, serializer.deserialize(serializer.serialize(p1)));
}

}

0 comments on commit d3c8821

Please sign in to comment.