From eb7dfbc7ef88561fb00b0af517b82f535181cd95 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 May 2022 14:28:19 +0200 Subject: [PATCH] Introduce JacksonObjectReader and JacksonObjectWriter function interfaces to customize JSON (de)serialization. We now encapsulate serialization and deserialization operations as JacksonObjectWriter and JacksonObjectReader functions to allow customization of Jackson serialization. Closes: #2322 Original Pull Request: #2332 --- .../GenericJackson2JsonRedisSerializer.java | 60 ++++++++++++++++--- .../Jackson2JsonRedisSerializer.java | 34 ++++++++++- .../redis/serializer/JacksonObjectReader.java | 57 ++++++++++++++++++ .../redis/serializer/JacksonObjectWriter.java | 54 +++++++++++++++++ ...cJackson2JsonRedisSerializerUnitTests.java | 34 +++++++++++ .../Jackson2JsonRedisSerializerTests.java | 11 ++++ 6 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java diff --git a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java index 23b513be1..16a298b06 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -25,7 +25,6 @@ import org.springframework.util.StringUtils; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; import com.fasterxml.jackson.databind.SerializerProvider; @@ -37,6 +36,9 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; /** * Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing. + *

+ * JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective + * {@link JacksonObjectWriter}. * * @author Christoph Strobl * @author Mark Paluch @@ -47,6 +49,10 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer T deserialize(@Nullable byte[] source, Class type) throws SerializationException { Assert.notNull(type, @@ -144,7 +188,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializerJackson's and * Jackson Databind {@link ObjectMapper}. *

- * This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. + * This serializer can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. * Note:Null objects are serialized as empty arrays and vice versa. + *

+ * JSON reading and writing can be customized by configuring {@link JacksonObjectReader} respective + * {@link JacksonObjectWriter}. * * @author Thomas Darimont + * @author Mark Paluch * @since 1.2 */ public class Jackson2JsonRedisSerializer implements RedisSerializer { @@ -45,6 +49,10 @@ public class Jackson2JsonRedisSerializer implements RedisSerializer { private ObjectMapper objectMapper = new ObjectMapper(); + private JacksonObjectReader reader = JacksonObjectReader.create(); + + private JacksonObjectWriter writer = JacksonObjectWriter.create(); + /** * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}. * @@ -70,7 +78,7 @@ public class Jackson2JsonRedisSerializer implements RedisSerializer { return null; } try { - return (T) this.objectMapper.readValue(bytes, 0, bytes.length, javaType); + return (T) this.reader.read(this.objectMapper, bytes, javaType); } catch (Exception ex) { throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); } @@ -83,7 +91,7 @@ public class Jackson2JsonRedisSerializer implements RedisSerializer { return SerializationUtils.EMPTY_ARRAY; } try { - return this.objectMapper.writeValueAsBytes(t); + return this.writer.write(this.objectMapper, t); } catch (Exception ex) { throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); } @@ -104,6 +112,26 @@ public class Jackson2JsonRedisSerializer implements RedisSerializer { this.objectMapper = objectMapper; } + /** + * Sets the {@link JacksonObjectReader} for this serializer. Setting the reader allows customization of the JSON + * deserialization. + * + * @since 3.0 + */ + public void setReader(JacksonObjectReader reader) { + this.reader = reader; + } + + /** + * Sets the {@link JacksonObjectWriter} for this serializer. Setting the reader allows customization of the JSON + * serialization. + * + * @since 3.0 + */ + public void setWriter(JacksonObjectWriter writer) { + this.writer = writer; + } + /** * Returns the Jackson {@link JavaType} for the specific class. *

diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java new file mode 100644 index 000000000..46552db37 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectReader.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 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 + * + * https://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.io.IOException; +import java.io.InputStream; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Defines the contract for Object Mapping readers. Implementations of this interface can deserialize a given byte array + * holding JSON to an Object considering the target type. + *

+ * Reader functions can customize how the actual JSON is being deserialized by e.g. obtaining a customized + * {@link com.fasterxml.jackson.databind.ObjectReader} applying serialization features, date formats, or views. + * + * @author Mark Paluch + * @since 3.0 + */ +@FunctionalInterface +public interface JacksonObjectReader { + + /** + * Read an object graph from the given root JSON into a Java object considering the {@link JavaType}. + * + * @param mapper the object mapper to use. + * @param source the JSON to deserialize. + * @param type the Java target type + * @return the deserialized Java object. + * @throws IOException if an I/O error or JSON deserialization error occurs. + */ + Object read(ObjectMapper mapper, byte[] source, JavaType type) throws IOException; + + /** + * Create a default {@link JacksonObjectReader} delegating to {@link ObjectMapper#readValue(InputStream, JavaType)}. + * + * @return the default {@link JacksonObjectReader}. + */ + static JacksonObjectReader create() { + return (mapper, source, type) -> mapper.readValue(source, 0, source.length, type); + } + +} diff --git a/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java new file mode 100644 index 000000000..64ecd44de --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/JacksonObjectWriter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2022 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 + * + * https://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.io.IOException; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Defines the contract for Object Mapping writers. Implementations of this interface can serialize a given Object to a + * {@code byte[]} containing JSON. + *

+ * Writer functions can customize how the actual JSON is being written by e.g. obtaining a customized + * {@link com.fasterxml.jackson.databind.ObjectWriter} applying serialization features, date formats, or views. + * + * @author Mark Paluch + * @since 3.0 + */ +@FunctionalInterface +public interface JacksonObjectWriter { + + /** + * Write the object graph with the given root {@code source} as byte array. + * + * @param mapper the object mapper to use. + * @param source the root of the object graph to marshal. + * @return a byte array containing the serialized object graph. + * @throws IOException if an I/O error or JSON serialization error occurs. + */ + byte[] write(ObjectMapper mapper, Object source) throws IOException; + + /** + * Create a default {@link JacksonObjectWriter} delegating to {@link ObjectMapper#writeValueAsBytes(Object)}. + * + * @return the default {@link JacksonObjectWriter}. + */ + static JacksonObjectWriter create() { + return ObjectMapper::writeValueAsBytes; + } + +} diff --git a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java index f13d7cf1a..9535e7512 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -26,11 +26,13 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import org.mockito.Mockito; + import org.springframework.beans.BeanUtils; import org.springframework.cache.support.NullValue; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -162,6 +164,24 @@ class GenericJackson2JsonRedisSerializerUnitTests { assertThat(serializer.deserialize(serializer.serialize(source))).isEqualTo(source); } + @Test // GH-2322 + void shouldConsiderWriter() { + + User user = new User(); + user.email = "walter@heisenberg.com"; + user.id = 42; + user.name = "Walter White"; + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null, + JacksonObjectReader.create(), (mapper, source) -> { + return mapper.writerWithView(Views.Basic.class).writeValueAsBytes(source); + }); + + byte[] result = serializer.serialize(user); + + assertThat(new String(result)).contains("id").contains("name").doesNotContain("email"); + } + private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) { NullValue nv = BeanUtils.instantiateClass(NullValue.class); @@ -252,4 +272,18 @@ class GenericJackson2JsonRedisSerializerUnitTests { } } + public class User { + @JsonView(Views.Basic.class) public int id; + @JsonView(Views.Basic.class) public String name; + @JsonView(Views.Detailed.class) public String email; + @JsonView(Views.Detailed.class) public String mobile; + } + + public class Views { + interface Basic {} + + interface Detailed {} + + } + } diff --git a/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java b/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java index f0d57830c..37982125a 100644 --- a/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java @@ -25,8 +25,11 @@ import org.springframework.data.redis.Person; import org.springframework.data.redis.PersonObjectFactory; /** + * Unit tests for {@link Jackson2JsonRedisSerializer}. + * * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ class Jackson2JsonRedisSerializerTests { @@ -64,4 +67,12 @@ class Jackson2JsonRedisSerializerTests { assertThatIllegalArgumentException().isThrownBy(() -> serializer.setObjectMapper(null)); } + @Test // GH-2322 + void shouldConsiderWriter() { + + Person person = new PersonObjectFactory().instance(); + serializer.setWriter((mapper, source) -> "foo".getBytes()); + assertThat(serializer.serialize(person)).isEqualTo("foo".getBytes()); + } + }