Refine Jackson2JsonRedisSerializer design.

Deprecate ObjectMapper setter. Introduce constructor accepting the ObjectMapper.

Original Pull Request: #2332
This commit is contained in:
Mark Paluch
2022-06-01 15:21:36 +02:00
committed by Christoph Strobl
parent 6f4ac1125a
commit c013a63ba9
3 changed files with 118 additions and 75 deletions

View File

@@ -32,7 +32,6 @@ 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;
@@ -55,37 +54,16 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
*/
public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Object> {
private ObjectMapper mapper;
private final ObjectMapper mapper;
private final JacksonObjectReader reader;
private final JacksonObjectWriter writer;
private boolean internalReader = false;
private final Lazy<Boolean> defaultTypingEnabled;
private final TypeResolver typeResolver;
private Lazy<Boolean> defaultTypingEnabled = Lazy
.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null);
private Lazy<String> 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 class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
*/
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create());
this.internalReader = true;
}
/**
@@ -122,7 +99,7 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
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 class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
} else {
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), DefaultTyping.EVERYTHING, As.PROPERTY);
}
if (classPropertyTypeName != null) {
typeHintPropertyName = Lazy.of(classPropertyTypeName);
}
}
/**
@@ -149,7 +122,6 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
*/
public GenericJackson2JsonRedisSerializer(ObjectMapper mapper) {
this(mapper, JacksonObjectReader.create(), JacksonObjectWriter.create());
this.internalReader = true;
}
/**
@@ -164,6 +136,11 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
*/
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 class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
this.mapper = mapper;
this.reader = reader;
this.writer = writer;
this.defaultTypingEnabled = Lazy.of(() -> mapper.getSerializationConfig().getDefaultTyper(null) != null);
Supplier<String> 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 class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
}
}
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> typeFactory;
private Supplier<String> hintName;
private final Supplier<String> hintName;
public TypeResolver(Supplier<TypeFactory> typeFactory, Supplier<String> hintName) {
@@ -259,15 +260,13 @@ public class GenericJackson2JsonRedisSerializer implements RedisSerializer<Objec
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);

View File

@@ -43,31 +43,86 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
*/
public class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
/**
* @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<T> 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<T> 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 class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
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 class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
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 class Jackson2JsonRedisSerializer<T> implements RedisSerializer<T> {
* 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;
}
/**