GH-1215: Allow Abstract Class Deserialization
Resolves https://github.com/spring-projects/spring-amqp/issues/1215 Previously, the message converter would fall back to header type info if the inferred type was abstract. Furthermore, we did not examine container type content being abstract. With a custom deserializer, abstract classes can be deserialized.
This commit is contained in:
committed by
Artem Bilan
parent
715f39ff3b
commit
06af4033c8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018-2019 the original author or authors.
|
||||
* Copyright 2018-2020 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.
|
||||
@@ -88,6 +88,8 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo
|
||||
|
||||
private boolean assumeSupportedContentType = true;
|
||||
|
||||
private boolean alwaysConvertToInferredType;
|
||||
|
||||
/**
|
||||
* Construct with the provided {@link ObjectMapper} instance.
|
||||
* @param objectMapper the {@link ObjectMapper} to use.
|
||||
@@ -201,6 +203,18 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When false (default), fall back to type id headers if the type (or contents of a container
|
||||
* type) is abstract. Set to true if conversion should always be attempted - perhaps because
|
||||
* a custom deserializer has been configured on the {@link ObjectMapper}. If the attempt fails,
|
||||
* fall back to headers.
|
||||
* @param alwaysAttemptConversion true to attempt.
|
||||
* @since 2.2.8
|
||||
*/
|
||||
public void setAlwaysConvertToInferredType(boolean alwaysAttemptConversion) {
|
||||
this.alwaysConvertToInferredType = alwaysAttemptConversion;
|
||||
}
|
||||
|
||||
protected boolean isUseProjectionForInterfaces() {
|
||||
return this.useProjectionForInterfaces;
|
||||
}
|
||||
@@ -274,29 +288,34 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo
|
||||
private Object doFromMessage(Message message, Object conversionHint, MessageProperties properties,
|
||||
String encoding) {
|
||||
|
||||
Object content;
|
||||
Object content = null;
|
||||
try {
|
||||
JavaType inferredType = this.javaTypeMapper.getInferredType(properties);
|
||||
if (inferredType != null && this.useProjectionForInterfaces && inferredType.isInterface()
|
||||
&& !inferredType.getRawClass().getPackage().getName().startsWith("java.util")) { // List etc
|
||||
content = this.projectingConverter.convert(message, inferredType.getRawClass());
|
||||
}
|
||||
else if (conversionHint instanceof ParameterizedTypeReference) {
|
||||
content = convertBytesToObject(message.getBody(), encoding,
|
||||
this.objectMapper.getTypeFactory().constructType(
|
||||
((ParameterizedTypeReference<?>) conversionHint).getType()));
|
||||
else if (inferredType != null && this.alwaysConvertToInferredType) {
|
||||
content = tryConverType(message, encoding, inferredType);
|
||||
}
|
||||
else if (getClassMapper() == null) {
|
||||
JavaType targetJavaType = getJavaTypeMapper()
|
||||
.toJavaType(message.getMessageProperties());
|
||||
content = convertBytesToObject(message.getBody(),
|
||||
encoding, targetJavaType);
|
||||
}
|
||||
else {
|
||||
Class<?> targetClass = getClassMapper().toClass(// NOSONAR never null
|
||||
message.getMessageProperties());
|
||||
content = convertBytesToObject(message.getBody(),
|
||||
encoding, targetClass);
|
||||
if (content == null) {
|
||||
if (conversionHint instanceof ParameterizedTypeReference) {
|
||||
content = convertBytesToObject(message.getBody(), encoding,
|
||||
this.objectMapper.getTypeFactory().constructType(
|
||||
((ParameterizedTypeReference<?>) conversionHint).getType()));
|
||||
}
|
||||
else if (getClassMapper() == null) {
|
||||
JavaType targetJavaType = getJavaTypeMapper()
|
||||
.toJavaType(message.getMessageProperties());
|
||||
content = convertBytesToObject(message.getBody(),
|
||||
encoding, targetJavaType);
|
||||
}
|
||||
else {
|
||||
Class<?> targetClass = getClassMapper().toClass(// NOSONAR never null
|
||||
message.getMessageProperties());
|
||||
content = convertBytesToObject(message.getBody(),
|
||||
encoding, targetClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
@@ -306,6 +325,21 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo
|
||||
return content;
|
||||
}
|
||||
|
||||
/*
|
||||
* Unfortunately, mapper.canDeserialize() always returns true (adds an AbstractDeserializer
|
||||
* to the cache); so all we can do is try a conversion.
|
||||
*/
|
||||
@Nullable
|
||||
private Object tryConverType(Message message, String encoding, JavaType inferredType) {
|
||||
try {
|
||||
return convertBytesToObject(message.getBody(), encoding, inferredType);
|
||||
}
|
||||
catch (Exception e) {
|
||||
this.log.trace("Cannot create possibly abstract container contents; falling back to headers", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException {
|
||||
String contentAsString = new String(body, encoding);
|
||||
return this.objectMapper.readValue(contentAsString, targetJavaType);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -112,9 +112,7 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem
|
||||
@Override
|
||||
public JavaType toJavaType(MessageProperties properties) {
|
||||
JavaType inferredType = getInferredType(properties);
|
||||
if (inferredType != null
|
||||
&& ((!inferredType.isAbstract() && !inferredType.isInterface()
|
||||
|| inferredType.getRawClass().getPackage().getName().startsWith("java.util")))) {
|
||||
if (inferredType != null && canConvert(inferredType)) {
|
||||
return inferredType;
|
||||
}
|
||||
|
||||
@@ -131,6 +129,19 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem
|
||||
return TypeFactory.defaultInstance().constructType(Object.class);
|
||||
}
|
||||
|
||||
private boolean canConvert(JavaType inferredType) {
|
||||
if (inferredType.isAbstract()) {
|
||||
return false;
|
||||
}
|
||||
if (inferredType.isContainerType() && inferredType.getContentType().isAbstract()) {
|
||||
return false;
|
||||
}
|
||||
if (inferredType.getKeyType() != null && inferredType.getKeyType().isAbstract()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeader) {
|
||||
JavaType classType = getClassIdType(typeIdHeader);
|
||||
if (!classType.isContainerType() || classType.isArrayType()) {
|
||||
@@ -151,7 +162,7 @@ public class DefaultJackson2JavaTypeMapper extends AbstractJavaTypeMapper implem
|
||||
@Override
|
||||
@Nullable
|
||||
public JavaType getInferredType(MessageProperties properties) {
|
||||
if (hasInferredTypeHeader(properties) && this.typePrecedence.equals(TypePrecedence.INFERRED)) {
|
||||
if (this.typePrecedence.equals(TypePrecedence.INFERRED) && hasInferredTypeHeader(properties)) {
|
||||
return fromInferredTypeHeader(properties);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.amqp.support.converter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Hashtable;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -34,7 +35,12 @@ import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.data.web.JsonPath;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
|
||||
|
||||
/**
|
||||
@@ -299,6 +305,75 @@ public class Jackson2JsonMessageConverterTests {
|
||||
assertThat(foo).isSameAs(bytes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customAbstractClass() {
|
||||
byte[] bytes = "{\"field\" : \"foo\" }".getBytes();
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.setHeader("__TypeId__", String.class.getName());
|
||||
messageProperties.setInferredArgumentType(Baz.class);
|
||||
Message message = new Message(bytes, messageProperties);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new BazModule());
|
||||
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
|
||||
j2Converter.setAlwaysConvertToInferredType(true);
|
||||
Baz baz = (Baz) j2Converter.fromMessage(message);
|
||||
assertThat(((Qux) baz).getField()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallbackToHeaders() {
|
||||
byte[] bytes = "{\"field\" : \"foo\" }".getBytes();
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.setHeader("__TypeId__", Buz.class.getName());
|
||||
messageProperties.setInferredArgumentType(Baz.class);
|
||||
Message message = new Message(bytes, messageProperties);
|
||||
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
|
||||
Fiz buz = (Fiz) j2Converter.fromMessage(message);
|
||||
assertThat(((Buz) buz).getField()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customAbstractClassList() throws Exception {
|
||||
byte[] bytes = "[{\"field\" : \"foo\" }]".getBytes();
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.setHeader("__TypeId__", String.class.getName());
|
||||
messageProperties.setInferredArgumentType(getClass().getDeclaredMethod("bazLister").getGenericReturnType());
|
||||
Message message = new Message(bytes, messageProperties);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new BazModule());
|
||||
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
|
||||
j2Converter.setAlwaysConvertToInferredType(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Baz> bazs = (List<Baz>) j2Converter.fromMessage(message);
|
||||
assertThat(bazs).hasSize(1);
|
||||
assertThat(((Qux) bazs.get(0)).getField()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cantDeserializeFizListUseHeaders() throws Exception {
|
||||
byte[] bytes = "[{\"field\" : \"foo\" }]".getBytes();
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
messageProperties.setInferredArgumentType(getClass().getDeclaredMethod("fizLister").getGenericReturnType());
|
||||
messageProperties.setHeader("__TypeId__", List.class.getName());
|
||||
messageProperties.setHeader("__ContentTypeId__", Buz.class.getName());
|
||||
Message message = new Message(bytes, messageProperties);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.registerModule(new BazModule());
|
||||
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Fiz> buzs = (List<Fiz>) j2Converter.fromMessage(message);
|
||||
assertThat(buzs).hasSize(1);
|
||||
assertThat(((Buz) buzs.get(0)).getField()).isEqualTo("foo");
|
||||
}
|
||||
|
||||
public List<Baz> bazLister() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Fiz> fizLister() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
|
||||
private String name = "foo";
|
||||
@@ -424,4 +499,73 @@ public class Jackson2JsonMessageConverterTests {
|
||||
|
||||
}
|
||||
|
||||
public interface Baz {
|
||||
|
||||
}
|
||||
|
||||
public static class Qux implements Baz {
|
||||
|
||||
private String field;
|
||||
|
||||
public Qux(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
public String getField() {
|
||||
return this.field;
|
||||
}
|
||||
|
||||
public void setField(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class BazDeserializer extends StdDeserializer<Baz> {
|
||||
|
||||
public BazDeserializer() {
|
||||
super(Baz.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Baz deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException, JsonProcessingException {
|
||||
|
||||
p.nextFieldName();
|
||||
String field = p.nextTextValue();
|
||||
p.nextToken();
|
||||
return new Qux(field);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface Fiz {
|
||||
|
||||
}
|
||||
|
||||
public static class Buz implements Fiz {
|
||||
|
||||
private String field;
|
||||
|
||||
public String getField() {
|
||||
return this.field;
|
||||
}
|
||||
|
||||
public void setField(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class BazModule extends SimpleModule {
|
||||
|
||||
public BazModule() {
|
||||
addDeserializer(Baz.class, new BazDeserializer());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3608,6 +3608,15 @@ converter to determine the type.
|
||||
IMPORTANT: Starting with version 1.6.11, `Jackson2JsonMessageConverter` and, therefore, `DefaultJackson2JavaTypeMapper` (`DefaultClassMapper`) provide the `trustedPackages` option to overcome https://pivotal.io/security/cve-2017-4995[Serialization Gadgets] vulnerability.
|
||||
By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option.
|
||||
|
||||
[[jackson-abstract]]
|
||||
====== Deserializing Abstract Classes
|
||||
|
||||
Prior to version 2.2.8, if the inferred type of a `@RabbitListener` was an abstract class (including interfaces), the converter would fall back to looking for type information in the headers and, if present, used that information; if that was not present, it would try to create the abstract class.
|
||||
This caused a problem when a custom `ObjectMapper` that is configured with a custom deserializer to handle the abstract class is used, but the incoming message has invalid type headers.
|
||||
|
||||
Starting with version 2.2.8, the previous behavior is retained by default. If you have such a custom `ObjectMapper` and you want to ignore type headers, and always use the inferred type for conversion, set the `alwaysConvertToInferredType` to `true`.
|
||||
This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`).
|
||||
|
||||
[[data-projection]]
|
||||
====== Using Spring Data Projection Interfaces
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ See <<change-history>> for changes in previous versions.
|
||||
Two additional connection factories are now provided.
|
||||
See <<choosing-factory>> for more information.
|
||||
|
||||
==== Message Converter Changes
|
||||
|
||||
The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer.
|
||||
See <<jackson-abstract>> for more information.
|
||||
|
||||
==== Testing Changes
|
||||
|
||||
A new annotation `@SpringRabbitTest` is provided to automatically configure some infrastructure beans for when you are not using `SpringBootTest`.
|
||||
|
||||
Reference in New Issue
Block a user