From 770ede23dda465ebd0d9db284bfaaabc7cf83902 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 May 2022 14:19:52 +0200 Subject: [PATCH 1/5] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4bd510786b..0c1a061422 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 3.0.0-SNAPSHOT + 3.0.0-GH-2322-SNAPSHOT Spring Data Redis Spring Data module for Redis From c2b5517bda790731be58c024176b0e6952f1ac69 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 24 May 2022 14:28:19 +0200 Subject: [PATCH 2/5] 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. --- .../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 8fca10fbd6..2121f52b73 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 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 @@ /** * 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 T deserialize(@Nullable byte[] source, Class type) throws Serializ } try { - return mapper.readValue(source, type); + return (T) reader.read(mapper, source, mapper.getTypeFactory().constructType(type)); } catch (Exception ex) { throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); } @@ -172,8 +216,7 @@ private static class NullValueSerializer extends StdSerializer { } @Override - public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) - throws IOException { + public void serialize(NullValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException { jgen.writeStartObject(); jgen.writeStringField(classIdentifier, NullValue.class.getName()); @@ -186,4 +229,5 @@ public void serializeWithType(NullValue value, JsonGenerator gen, SerializerProv serialize(value, gen, serializers); } } + } diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java index ed41ea1462..a3140bd940 100644 --- a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java @@ -31,10 +31,14 @@ * Jackson'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 T deserialize(@Nullable byte[] bytes) throws SerializationException { 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 byte[] serialize(@Nullable Object t) throws SerializationException { 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 void setObjectMapper(ObjectMapper objectMapper) { 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 0000000000..46552db37d --- /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 0000000000..64ecd44de9 --- /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 f13d7cf1a8..9535e7512d 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 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 @@ void deserializeShouldBeAbleToRestoreFinalObjectAfterSerialization() { 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 @@ public boolean equals(Object obj) { } } + 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 f0d57830ce..37982125ad 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.PersonObjectFactory; /** + * Unit tests for {@link Jackson2JsonRedisSerializer}. + * * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ class Jackson2JsonRedisSerializerTests { @@ -64,4 +67,12 @@ void testJackson2JsonSerilizerThrowsExceptionWhenSettingNullObjectMapper() { 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()); + } + } From b366c8cc474dd08aa63a0585ec78243f6bb28eb9 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 31 May 2022 12:57:33 +0200 Subject: [PATCH 3/5] Reader should provide target Object type to allow easy customization. This commit makes sure to extract and pass on the target type, otherwise it would be Object.class all the time. --- .../GenericJackson2JsonRedisSerializer.java | 85 ++++++++++++++++++- ...cJackson2JsonRedisSerializerUnitTests.java | 35 +++++++- 2 files changed, 115 insertions(+), 5 deletions(-) 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 2121f52b73..5a4f2f8540 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -16,8 +16,11 @@ package org.springframework.data.redis.serializer; import java.io.IOException; +import java.util.Collections; +import java.util.function.Supplier; import org.springframework.cache.support.NullValue; +import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -25,14 +28,19 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.TextNode; import com.fasterxml.jackson.databind.ser.SerializerFactory; import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; /** * Generic Jackson 2-based {@link RedisSerializer} that maps {@link Object objects} to JSON using dynamic typing. @@ -47,12 +55,37 @@ */ public class GenericJackson2JsonRedisSerializer implements RedisSerializer { - private final ObjectMapper mapper; + private ObjectMapper mapper; private final JacksonObjectReader reader; private final JacksonObjectWriter writer; + private boolean internalReader = false; + + private final TypeResolver typeResolver; + + private Lazy defaultTypingEnabled = Lazy + .of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null); + + private Lazy typeHintPropertyName; + + { + typeHintPropertyName = Lazy.of(() -> { + if (defaultTypingEnabled.get()) { + return null; + } + + return mapper.getDeserializationConfig().getDefaultTyper(null) + .buildTypeDeserializer(mapper.getDeserializationConfig(), mapper.getTypeFactory().constructType(Object.class), + Collections.emptyList()) + .getPropertyName(); + + }).or("@class"); + + typeResolver = new TypeResolver(Lazy.of(() -> mapper.getTypeFactory()), typeHintPropertyName); + } + /** * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing. */ @@ -71,6 +104,7 @@ public GenericJackson2JsonRedisSerializer() { */ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) { this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create()); + this.internalReader = true; } /** @@ -100,6 +134,10 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName } else { mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY); } + + if (classPropertyTypeName != null) { + typeHintPropertyName = Lazy.of(classPropertyTypeName); + } } /** @@ -111,6 +149,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName */ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) { this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create()); + this.internalReader = true; } /** @@ -188,12 +227,54 @@ public T deserialize(@Nullable byte[] source, Class type) throws Serializ } try { - return (T) reader.read(mapper, source, mapper.getTypeFactory().constructType(type)); + return (T) reader.read(mapper, source, resolveType(source, type)); } catch (Exception ex) { throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); } } + protected JavaType resolveType(byte[] source, Class type) { + + if (internalReader || !type.equals(Object.class) || !defaultTypingEnabled.get()) { + return typeResolver.constructType(type); + } + + return typeResolver.resolveType(source, type); + } + + private static class TypeResolver { + + private final ObjectReader objectReader = new ObjectMapper().reader(); + + private final Supplier typeFactory; + private Supplier hintName; + + public TypeResolver(Supplier typeFactory, Supplier hintName) { + + this.typeFactory = typeFactory; + this.hintName = hintName; + } + + protected JavaType constructType(Class type) { + return typeFactory.get().constructType(type); + } + + protected JavaType resolveType(byte[] source, Class type) { + + try { + TextNode typeName = (TextNode) objectReader.readValue(source, JsonNode.class).get(hintName.get()); + if (typeName != null) { + return typeFactory.get().constructFromCanonical(typeName.textValue()); + } + } catch (IOException e) { + // TODO: logging? + } + + return constructType(type); + } + + } + /** * {@link StdSerializer} adding class information required by default typing. This allows de-/serialization of * {@link NullValue}. 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 9535e7512d..ae565f7eab 100644 --- a/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializerUnitTests.java @@ -21,12 +21,12 @@ import static org.springframework.util.ObjectUtils.*; import lombok.Data; +import lombok.ToString; 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; @@ -182,6 +182,34 @@ void shouldConsiderWriter() { assertThat(new String(result)).contains("id").contains("name").doesNotContain("email"); } + @Test // GH-2322 + void shouldConsiderReader() { + + User user = new User(); + user.email = "walter@heisenberg.com"; + user.id = 42; + user.name = "Walter White"; + + GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer((String) null, + (mapper, source, type) -> { + if (type.getRawClass() == User.class) { + return mapper.readerWithView(Views.Basic.class).forType(type).readValue(source); + } + return mapper.readValue(source, type); + }, JacksonObjectWriter.create()); + + byte[] serializedValue = serializer.serialize(user); + + Object result = serializer.deserialize(serializedValue); + assertThat(result).isInstanceOf(User.class).satisfies(it -> { + User u = (User) it; + assertThat(u.id).isEqualTo(user.id); + assertThat(u.name).isEqualTo(user.name); + assertThat(u.email).isNull(); + assertThat(u.mobile).isNull(); + }); + } + private static void serializeAndDeserializeNullValue(GenericJackson2JsonRedisSerializer serializer) { NullValue nv = BeanUtils.instantiateClass(NullValue.class); @@ -272,14 +300,15 @@ public boolean equals(Object obj) { } } - public class User { + @ToString + static 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 { + static class Views { interface Basic {} interface Detailed {} From d14cca9e6205443df20e1d32aee1ab94e167f975 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 1 Jun 2022 15:21:36 +0200 Subject: [PATCH 4/5] Polishing. Introduce single private constructor to accept all arguments. --- .../GenericJackson2JsonRedisSerializer.java | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) 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 5a4f2f8540..9b0aa53e54 100644 --- a/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/GenericJackson2JsonRedisSerializer.java @@ -32,7 +32,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; import com.fasterxml.jackson.databind.jsontype.TypeSerializer; @@ -55,37 +54,16 @@ */ public class GenericJackson2JsonRedisSerializer implements RedisSerializer { - private ObjectMapper mapper; + private final ObjectMapper mapper; private final JacksonObjectReader reader; private final JacksonObjectWriter writer; - private boolean internalReader = false; + private final Lazy defaultTypingEnabled; private final TypeResolver typeResolver; - private Lazy defaultTypingEnabled = Lazy - .of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null); - - private Lazy typeHintPropertyName; - - { - typeHintPropertyName = Lazy.of(() -> { - if (defaultTypingEnabled.get()) { - return null; - } - - return mapper.getDeserializationConfig().getDefaultTyper(null) - .buildTypeDeserializer(mapper.getDeserializationConfig(), mapper.getTypeFactory().constructType(Object.class), - Collections.emptyList()) - .getPropertyName(); - - }).or("@class"); - - typeResolver = new TypeResolver(Lazy.of(() -> mapper.getTypeFactory()), typeHintPropertyName); - } - /** * Creates {@link GenericJackson2JsonRedisSerializer} and configures {@link ObjectMapper} for default typing. */ @@ -104,7 +82,6 @@ public GenericJackson2JsonRedisSerializer() { */ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) { this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create()); - this.internalReader = true; } /** @@ -122,7 +99,7 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName, JacksonObjectReader reader, JacksonObjectWriter writer) { - this(new ObjectMapper(), reader, writer); + this(new ObjectMapper(), reader, writer, classPropertyTypeName); // simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need // the type hint embedded for deserialization using the default typing feature. @@ -134,10 +111,6 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName } else { mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY); } - - if (classPropertyTypeName != null) { - typeHintPropertyName = Lazy.of(classPropertyTypeName); - } } /** @@ -149,7 +122,6 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName */ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) { this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create()); - this.internalReader = true; } /** @@ -164,6 +136,11 @@ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) { */ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, JacksonObjectWriter writer) { + this(mapper, reader, writer, null); + } + + private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectReader reader, + JacksonObjectWriter writer, @Nullable String typeHintPropertyName) { Assert.notNull(mapper, "ObjectMapper must not be null!"); Assert.notNull(reader, "Reader must not be null!"); @@ -172,6 +149,29 @@ public GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectRead this.mapper = mapper; this.reader = reader; this.writer = writer; + + this.defaultTypingEnabled = Lazy.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null); + + Supplier typeHintPropertyNameSupplier; + + if (typeHintPropertyName == null) { + + typeHintPropertyNameSupplier = Lazy.of(() -> { + if (defaultTypingEnabled.get()) { + return null; + } + + return mapper.getDeserializationConfig().getDefaultTyper(null) + .buildTypeDeserializer(mapper.getDeserializationConfig(), + mapper.getTypeFactory().constructType(Object.class), Collections.emptyList()) + .getPropertyName(); + + }).or("@class"); + } else { + typeHintPropertyNameSupplier = () -> typeHintPropertyName; + } + + this.typeResolver = new TypeResolver(Lazy.of(mapper::getTypeFactory), typeHintPropertyNameSupplier); } /** @@ -233,21 +233,22 @@ public T deserialize(@Nullable byte[] source, Class type) throws Serializ } } - protected JavaType resolveType(byte[] source, Class type) { + protected JavaType resolveType(byte[] source, Class type) throws IOException { - if (internalReader || !type.equals(Object.class) || !defaultTypingEnabled.get()) { + if (!type.equals(Object.class) || !defaultTypingEnabled.get()) { return typeResolver.constructType(type); } return typeResolver.resolveType(source, type); } - private static class TypeResolver { + static class TypeResolver { - private final ObjectReader objectReader = new ObjectMapper().reader(); + // need a separate instance to bypass class hint checks + private final ObjectMapper mapper = new ObjectMapper(); private final Supplier typeFactory; - private Supplier hintName; + private final Supplier hintName; public TypeResolver(Supplier typeFactory, Supplier hintName) { @@ -259,15 +260,13 @@ protected JavaType constructType(Class type) { return typeFactory.get().constructType(type); } - protected JavaType resolveType(byte[] source, Class type) { + protected JavaType resolveType(byte[] source, Class type) throws IOException { - try { - TextNode typeName = (TextNode) objectReader.readValue(source, JsonNode.class).get(hintName.get()); - if (typeName != null) { - return typeFactory.get().constructFromCanonical(typeName.textValue()); - } - } catch (IOException e) { - // TODO: logging? + JsonNode root = mapper.readTree(source); + JsonNode jsonNode = root.get(hintName.get()); + + if (jsonNode instanceof TextNode && jsonNode.asText() != null) { + return typeFactory.get().constructFromCanonical(jsonNode.asText()); } return constructType(type); From ed7bc0814cffe6a8d3ac4047b1492d488c47d3bc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 23 Jun 2022 14:15:22 +0200 Subject: [PATCH 5/5] Refine Jackson2JsonRedisSerializer design. Deprecate ObjectMapper setter. Introduce constructor accepting the ObjectMapper. --- .../Jackson2JsonRedisSerializer.java | 99 +++++++++++++------ .../Jackson2JsonRedisSerializerTests.java | 7 +- 2 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java index a3140bd940..614b57d61e 100644 --- a/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java +++ b/src/main/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializer.java @@ -43,31 +43,86 @@ */ public class Jackson2JsonRedisSerializer implements RedisSerializer { + /** + * @deprecated since 3.0 for removal. + */ + @Deprecated(since = "3.0", forRemoval = true) // public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private final JavaType javaType; - private ObjectMapper objectMapper = new ObjectMapper(); + private ObjectMapper mapper; - private JacksonObjectReader reader = JacksonObjectReader.create(); + private final JacksonObjectReader reader; - private JacksonObjectWriter writer = JacksonObjectWriter.create(); + private final JacksonObjectWriter writer; /** * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}. * - * @param type + * @param type must not be {@literal null}. */ public Jackson2JsonRedisSerializer(Class type) { - this.javaType = getJavaType(type); + this(new ObjectMapper(), type); } /** * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}. * - * @param javaType + * @param javaType must not be {@literal null}. */ public Jackson2JsonRedisSerializer(JavaType javaType) { + this(new ObjectMapper(), javaType); + } + + /** + * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link Class}. + * + * @param mapper must not be {@literal null}. + * @param type must not be {@literal null}. + * @since 3.0 + */ + public Jackson2JsonRedisSerializer(ObjectMapper mapper, Class type) { + + Assert.notNull(mapper, "ObjectMapper must not be null"); + Assert.notNull(type, "Java type must not be null"); + + this.javaType = getJavaType(type); + this.mapper = mapper; + this.reader = JacksonObjectReader.create(); + this.writer = JacksonObjectWriter.create(); + } + + /** + * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}. + * + * @param mapper must not be {@literal null}. + * @param javaType must not be {@literal null}. + * @since 3.0 + */ + public Jackson2JsonRedisSerializer(ObjectMapper mapper, JavaType javaType) { + this(mapper, javaType, JacksonObjectReader.create(), JacksonObjectWriter.create()); + } + + /** + * Creates a new {@link Jackson2JsonRedisSerializer} for the given target {@link JavaType}. + * + * @param mapper must not be {@literal null}. + * @param javaType must not be {@literal null}. + * @param reader the {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}. + * @param writer the {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}. + * @since 3.0 + */ + public Jackson2JsonRedisSerializer(ObjectMapper mapper, JavaType javaType, JacksonObjectReader reader, + JacksonObjectWriter writer) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + Assert.notNull(reader, "Reader must not be null!"); + Assert.notNull(writer, "Writer must not be null!"); + + this.mapper = mapper; + this.reader = reader; + this.writer = writer; this.javaType = javaType; } @@ -78,7 +133,7 @@ public T deserialize(@Nullable byte[] bytes) throws SerializationException { return null; } try { - return (T) this.reader.read(this.objectMapper, bytes, javaType); + return (T) this.reader.read(this.mapper, bytes, javaType); } catch (Exception ex) { throw new SerializationException("Could not read JSON: " + ex.getMessage(), ex); } @@ -91,7 +146,7 @@ public byte[] serialize(@Nullable Object t) throws SerializationException { return SerializationUtils.EMPTY_ARRAY; } try { - return this.writer.write(this.objectMapper, t); + return this.writer.write(this.mapper, t); } catch (Exception ex) { throw new SerializationException("Could not write JSON: " + ex.getMessage(), ex); } @@ -105,31 +160,15 @@ public byte[] serialize(@Nullable Object t) throws SerializationException { * 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; - } - - /** - * Sets the {@link JacksonObjectReader} for this serializer. Setting the reader allows customization of the JSON - * deserialization. * - * @since 3.0 + * @deprecated since 3.0, use {@link #Jackson2JsonRedisSerializer(ObjectMapper, Class) constructor creation} to + * configure the object mapper. */ - public void setReader(JacksonObjectReader reader) { - this.reader = reader; - } + @Deprecated(since = "3.0", forRemoval = true) + public void setObjectMapper(ObjectMapper mapper) { - /** - * 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; + Assert.notNull(mapper, "'objectMapper' must not be null"); + this.mapper = mapper; } /** 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 37982125ad..1da6e10b8f 100644 --- a/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java +++ b/src/test/java/org/springframework/data/redis/serializer/Jackson2JsonRedisSerializerTests.java @@ -24,6 +24,9 @@ import org.springframework.data.redis.Person; import org.springframework.data.redis.PersonObjectFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; + /** * Unit tests for {@link Jackson2JsonRedisSerializer}. * @@ -70,8 +73,10 @@ void testJackson2JsonSerilizerThrowsExceptionWhenSettingNullObjectMapper() { @Test // GH-2322 void shouldConsiderWriter() { + serializer = new Jackson2JsonRedisSerializer<>(new ObjectMapper(), + TypeFactory.defaultInstance().constructType(Person.class), JacksonObjectReader.create(), + (mapper, source) -> "foo".getBytes()); Person person = new PersonObjectFactory().instance(); - serializer.setWriter((mapper, source) -> "foo".getBytes()); assertThat(serializer.serialize(person)).isEqualTo("foo".getBytes()); }