From 6f4ac1125a6ef65103339a46ade3bb0dd11e400b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 31 May 2022 12:57:33 +0200 Subject: [PATCH] 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. Original Pull Request: #2332 --- .../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 16a298b06..d6254443f 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 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.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 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; */ 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 class GenericJackson2JsonRedisSerializer implements RedisSerializer 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 9535e7512..ae565f7ea 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.test.util.ReflectionTestUtils.*; 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 @@ class GenericJackson2JsonRedisSerializerUnitTests { 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 @@ class GenericJackson2JsonRedisSerializerUnitTests { } } - 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 {}