Skip to content

Commit fe05179

Browse files
garyrussellartembilan
authored andcommitted
GH-1215: Allow Abstract Class Deserialization
Resolves #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. # Conflicts: # src/reference/asciidoc/whats-new.adoc
1 parent 709a34b commit fe05179

File tree

5 files changed

+223
-23
lines changed

5 files changed

+223
-23
lines changed

spring-amqp/src/main/java/org/springframework/amqp/support/converter/AbstractJackson2MessageConverter.java

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018-2019 the original author or authors.
2+
* Copyright 2018-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,6 +88,8 @@ public abstract class AbstractJackson2MessageConverter extends AbstractMessageCo
8888

8989
private boolean assumeSupportedContentType = true;
9090

91+
private boolean alwaysConvertToInferredType;
92+
9193
/**
9294
* Construct with the provided {@link ObjectMapper} instance.
9395
* @param objectMapper the {@link ObjectMapper} to use.
@@ -201,6 +203,18 @@ public void setTypePrecedence(Jackson2JavaTypeMapper.TypePrecedence typePreceden
201203
}
202204
}
203205

206+
/**
207+
* When false (default), fall back to type id headers if the type (or contents of a container
208+
* type) is abstract. Set to true if conversion should always be attempted - perhaps because
209+
* a custom deserializer has been configured on the {@link ObjectMapper}. If the attempt fails,
210+
* fall back to headers.
211+
* @param alwaysAttemptConversion true to attempt.
212+
* @since 2.2.8
213+
*/
214+
public void setAlwaysConvertToInferredType(boolean alwaysAttemptConversion) {
215+
this.alwaysConvertToInferredType = alwaysAttemptConversion;
216+
}
217+
204218
protected boolean isUseProjectionForInterfaces() {
205219
return this.useProjectionForInterfaces;
206220
}
@@ -274,29 +288,34 @@ public Object fromMessage(Message message, @Nullable Object conversionHint) thro
274288
private Object doFromMessage(Message message, Object conversionHint, MessageProperties properties,
275289
String encoding) {
276290

277-
Object content;
291+
Object content = null;
278292
try {
279293
JavaType inferredType = this.javaTypeMapper.getInferredType(properties);
280294
if (inferredType != null && this.useProjectionForInterfaces && inferredType.isInterface()
281295
&& !inferredType.getRawClass().getPackage().getName().startsWith("java.util")) { // List etc
282296
content = this.projectingConverter.convert(message, inferredType.getRawClass());
283297
}
284-
else if (conversionHint instanceof ParameterizedTypeReference) {
285-
content = convertBytesToObject(message.getBody(), encoding,
286-
this.objectMapper.getTypeFactory().constructType(
287-
((ParameterizedTypeReference<?>) conversionHint).getType()));
288-
}
289-
else if (getClassMapper() == null) {
290-
JavaType targetJavaType = getJavaTypeMapper()
291-
.toJavaType(message.getMessageProperties());
292-
content = convertBytesToObject(message.getBody(),
293-
encoding, targetJavaType);
298+
else if (inferredType != null && this.alwaysConvertToInferredType) {
299+
content = tryConverType(message, encoding, inferredType);
294300
}
295-
else {
296-
Class<?> targetClass = getClassMapper().toClass(// NOSONAR never null
297-
message.getMessageProperties());
298-
content = convertBytesToObject(message.getBody(),
299-
encoding, targetClass);
301+
if (content == null) {
302+
if (conversionHint instanceof ParameterizedTypeReference) {
303+
content = convertBytesToObject(message.getBody(), encoding,
304+
this.objectMapper.getTypeFactory().constructType(
305+
((ParameterizedTypeReference<?>) conversionHint).getType()));
306+
}
307+
else if (getClassMapper() == null) {
308+
JavaType targetJavaType = getJavaTypeMapper()
309+
.toJavaType(message.getMessageProperties());
310+
content = convertBytesToObject(message.getBody(),
311+
encoding, targetJavaType);
312+
}
313+
else {
314+
Class<?> targetClass = getClassMapper().toClass(// NOSONAR never null
315+
message.getMessageProperties());
316+
content = convertBytesToObject(message.getBody(),
317+
encoding, targetClass);
318+
}
300319
}
301320
}
302321
catch (IOException e) {
@@ -306,6 +325,21 @@ else if (getClassMapper() == null) {
306325
return content;
307326
}
308327

328+
/*
329+
* Unfortunately, mapper.canDeserialize() always returns true (adds an AbstractDeserializer
330+
* to the cache); so all we can do is try a conversion.
331+
*/
332+
@Nullable
333+
private Object tryConverType(Message message, String encoding, JavaType inferredType) {
334+
try {
335+
return convertBytesToObject(message.getBody(), encoding, inferredType);
336+
}
337+
catch (Exception e) {
338+
this.log.trace("Cannot create possibly abstract container contents; falling back to headers", e);
339+
return null;
340+
}
341+
}
342+
309343
private Object convertBytesToObject(byte[] body, String encoding, JavaType targetJavaType) throws IOException {
310344
String contentAsString = new String(body, encoding);
311345
return this.objectMapper.readValue(contentAsString, targetJavaType);

spring-amqp/src/main/java/org/springframework/amqp/support/converter/DefaultJackson2JavaTypeMapper.java

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -112,9 +112,7 @@ public void addTrustedPackages(@Nullable String... packages) {
112112
@Override
113113
public JavaType toJavaType(MessageProperties properties) {
114114
JavaType inferredType = getInferredType(properties);
115-
if (inferredType != null
116-
&& ((!inferredType.isAbstract() && !inferredType.isInterface()
117-
|| inferredType.getRawClass().getPackage().getName().startsWith("java.util")))) {
115+
if (inferredType != null && canConvert(inferredType)) {
118116
return inferredType;
119117
}
120118

@@ -131,6 +129,19 @@ public JavaType toJavaType(MessageProperties properties) {
131129
return TypeFactory.defaultInstance().constructType(Object.class);
132130
}
133131

132+
private boolean canConvert(JavaType inferredType) {
133+
if (inferredType.isAbstract()) {
134+
return false;
135+
}
136+
if (inferredType.isContainerType() && inferredType.getContentType().isAbstract()) {
137+
return false;
138+
}
139+
if (inferredType.getKeyType() != null && inferredType.getKeyType().isAbstract()) {
140+
return false;
141+
}
142+
return true;
143+
}
144+
134145
private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeader) {
135146
JavaType classType = getClassIdType(typeIdHeader);
136147
if (!classType.isContainerType() || classType.isArrayType()) {
@@ -151,7 +162,7 @@ private JavaType fromTypeHeader(MessageProperties properties, String typeIdHeade
151162
@Override
152163
@Nullable
153164
public JavaType getInferredType(MessageProperties properties) {
154-
if (hasInferredTypeHeader(properties) && this.typePrecedence.equals(TypePrecedence.INFERRED)) {
165+
if (this.typePrecedence.equals(TypePrecedence.INFERRED) && hasInferredTypeHeader(properties)) {
155166
return fromInferredTypeHeader(properties);
156167
}
157168
return null;

spring-amqp/src/test/java/org/springframework/amqp/support/converter/Jackson2JsonMessageConverterTests.java

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020

21+
import java.io.IOException;
2122
import java.math.BigDecimal;
2223
import java.util.Hashtable;
2324
import java.util.LinkedHashMap;
@@ -34,7 +35,12 @@
3435
import org.springframework.data.web.JsonPath;
3536
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3637

38+
import com.fasterxml.jackson.core.JsonParser;
39+
import com.fasterxml.jackson.core.JsonProcessingException;
40+
import com.fasterxml.jackson.databind.DeserializationContext;
3741
import com.fasterxml.jackson.databind.ObjectMapper;
42+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
43+
import com.fasterxml.jackson.databind.module.SimpleModule;
3844
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
3945

4046
/**
@@ -299,6 +305,75 @@ public void testMissingContentType() {
299305
assertThat(foo).isSameAs(bytes);
300306
}
301307

308+
@Test
309+
void customAbstractClass() {
310+
byte[] bytes = "{\"field\" : \"foo\" }".getBytes();
311+
MessageProperties messageProperties = new MessageProperties();
312+
messageProperties.setHeader("__TypeId__", String.class.getName());
313+
messageProperties.setInferredArgumentType(Baz.class);
314+
Message message = new Message(bytes, messageProperties);
315+
ObjectMapper mapper = new ObjectMapper();
316+
mapper.registerModule(new BazModule());
317+
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
318+
j2Converter.setAlwaysConvertToInferredType(true);
319+
Baz baz = (Baz) j2Converter.fromMessage(message);
320+
assertThat(((Qux) baz).getField()).isEqualTo("foo");
321+
}
322+
323+
@Test
324+
void fallbackToHeaders() {
325+
byte[] bytes = "{\"field\" : \"foo\" }".getBytes();
326+
MessageProperties messageProperties = new MessageProperties();
327+
messageProperties.setHeader("__TypeId__", Buz.class.getName());
328+
messageProperties.setInferredArgumentType(Baz.class);
329+
Message message = new Message(bytes, messageProperties);
330+
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter();
331+
Fiz buz = (Fiz) j2Converter.fromMessage(message);
332+
assertThat(((Buz) buz).getField()).isEqualTo("foo");
333+
}
334+
335+
@Test
336+
void customAbstractClassList() throws Exception {
337+
byte[] bytes = "[{\"field\" : \"foo\" }]".getBytes();
338+
MessageProperties messageProperties = new MessageProperties();
339+
messageProperties.setHeader("__TypeId__", String.class.getName());
340+
messageProperties.setInferredArgumentType(getClass().getDeclaredMethod("bazLister").getGenericReturnType());
341+
Message message = new Message(bytes, messageProperties);
342+
ObjectMapper mapper = new ObjectMapper();
343+
mapper.registerModule(new BazModule());
344+
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
345+
j2Converter.setAlwaysConvertToInferredType(true);
346+
@SuppressWarnings("unchecked")
347+
List<Baz> bazs = (List<Baz>) j2Converter.fromMessage(message);
348+
assertThat(bazs).hasSize(1);
349+
assertThat(((Qux) bazs.get(0)).getField()).isEqualTo("foo");
350+
}
351+
352+
@Test
353+
void cantDeserializeFizListUseHeaders() throws Exception {
354+
byte[] bytes = "[{\"field\" : \"foo\" }]".getBytes();
355+
MessageProperties messageProperties = new MessageProperties();
356+
messageProperties.setInferredArgumentType(getClass().getDeclaredMethod("fizLister").getGenericReturnType());
357+
messageProperties.setHeader("__TypeId__", List.class.getName());
358+
messageProperties.setHeader("__ContentTypeId__", Buz.class.getName());
359+
Message message = new Message(bytes, messageProperties);
360+
ObjectMapper mapper = new ObjectMapper();
361+
mapper.registerModule(new BazModule());
362+
Jackson2JsonMessageConverter j2Converter = new Jackson2JsonMessageConverter(mapper);
363+
@SuppressWarnings("unchecked")
364+
List<Fiz> buzs = (List<Fiz>) j2Converter.fromMessage(message);
365+
assertThat(buzs).hasSize(1);
366+
assertThat(((Buz) buzs.get(0)).getField()).isEqualTo("foo");
367+
}
368+
369+
public List<Baz> bazLister() {
370+
return null;
371+
}
372+
373+
public List<Fiz> fizLister() {
374+
return null;
375+
}
376+
302377
public static class Foo {
303378

304379
private String name = "foo";
@@ -424,4 +499,73 @@ interface Sample {
424499

425500
}
426501

502+
public interface Baz {
503+
504+
}
505+
506+
public static class Qux implements Baz {
507+
508+
private String field;
509+
510+
public Qux(String field) {
511+
this.field = field;
512+
}
513+
514+
public String getField() {
515+
return this.field;
516+
}
517+
518+
public void setField(String field) {
519+
this.field = field;
520+
}
521+
522+
}
523+
524+
@SuppressWarnings("serial")
525+
public static class BazDeserializer extends StdDeserializer<Baz> {
526+
527+
public BazDeserializer() {
528+
super(Baz.class);
529+
}
530+
531+
@Override
532+
public Baz deserialize(JsonParser p, DeserializationContext ctxt)
533+
throws IOException, JsonProcessingException {
534+
535+
p.nextFieldName();
536+
String field = p.nextTextValue();
537+
p.nextToken();
538+
return new Qux(field);
539+
540+
}
541+
542+
}
543+
544+
public interface Fiz {
545+
546+
}
547+
548+
public static class Buz implements Fiz {
549+
550+
private String field;
551+
552+
public String getField() {
553+
return this.field;
554+
}
555+
556+
public void setField(String field) {
557+
this.field = field;
558+
}
559+
560+
}
561+
562+
@SuppressWarnings("serial")
563+
public static class BazModule extends SimpleModule {
564+
565+
public BazModule() {
566+
addDeserializer(Baz.class, new BazDeserializer());
567+
}
568+
569+
}
570+
427571
}

src/reference/asciidoc/amqp.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3553,6 +3553,15 @@ converter to determine the type.
35533553
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.
35543554
By default and for backward compatibility, the `Jackson2JsonMessageConverter` trusts all packages -- that is, it uses `*` for the option.
35553555

3556+
[[jackson-abstract]]
3557+
====== Deserializing Abstract Classes
3558+
3559+
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.
3560+
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.
3561+
3562+
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`.
3563+
This is needed for backwards compatibility and to avoid the overhead of an attempted conversion when it would fail (with a standard `ObjectMapper`).
3564+
35563565
[[data-projection]]
35573566
====== Using Spring Data Projection Interfaces
35583567

src/reference/asciidoc/whats-new.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ The `CachingConnectionFactory` has a new property `shuffleAddresses`.
9696
When providing a list of broker node addresses, the list will be shuffled before creating a connection so that the order in which the connections are attempted is random.
9797
See <<cluster>> for more information.
9898

99+
==== Testing Changes
100+
99101
When using Publisher confirms and returns, the callbacks are now invoked on the connection factory's `executor`.
100102
This avoids a possible deadlock in the `amqp-clients` library if you perform rabbit operations from within the callback.
101103
See <<template-confirms>> for more information.

0 commit comments

Comments
 (0)