Skip to content

Commit 9c46b34

Browse files
authored
Fix JsonToObjectTransformer for ClassNotFoundEx (#3228)
* Fix JsonToObjectTransformer for ClassNotFoundEx Related to #3223 The `JsonToObjectTransformer` consults `JsonHeaders` first and tries to build a `ResolvableType` from other type headers which may be just a string identifications. In this case a `ClassNotFoundException` could be thrown if a `ResolvableType` cannot be build against non-class identificators * Add `valueTypeExpression` option to the `JsonToObjectTransformer` to let to build a `ResolvableType` using any possible custom logic, e.g. resolving target classes from some registry using their ids from the mentioned headers **Cherry-pick to 5.2.x** * * Fix English language mistakes
1 parent f1a0a06 commit 9c46b34

File tree

7 files changed

+128
-27
lines changed

7 files changed

+128
-27
lines changed

spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java

Lines changed: 6 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.
@@ -26,6 +26,7 @@
2626
/**
2727
* @author Mark Fisher
2828
* @author Artem Bilan
29+
*
2930
* @since 2.0
3031
*/
3132
public class JsonToObjectTransformerParser extends AbstractTransformerParser {
@@ -45,6 +46,10 @@ protected void parseTransformer(Element element, ParserContext parserContext, Be
4546
if (StringUtils.hasText(objectMapper)) {
4647
builder.addConstructorArgReference(objectMapper);
4748
}
49+
50+
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "value-type-expression",
51+
"valueTypeExpressionString");
52+
4853
}
4954

5055
}

spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
import org.springframework.beans.factory.BeanClassLoaderAware;
2323
import org.springframework.core.ResolvableType;
24+
import org.springframework.expression.EvaluationContext;
25+
import org.springframework.expression.Expression;
26+
import org.springframework.integration.expression.ExpressionUtils;
27+
import org.springframework.integration.expression.FunctionExpression;
2428
import org.springframework.integration.mapping.support.JsonHeaders;
2529
import org.springframework.integration.support.json.JsonObjectMapper;
2630
import org.springframework.integration.support.json.JsonObjectMapperProvider;
@@ -37,10 +41,12 @@
3741
* factory to get an instance of Jackson JSON-processor
3842
* if jackson-databind lib is present on the classpath. Any other {@linkplain JsonObjectMapper}
3943
* implementation can be provided.
40-
* <p>Since version 3.0, you can omit the target class and the target type can be
44+
* <p> Since version 3.0, you can omit the target class and the target type can be
4145
* determined by the {@link JsonHeaders} type entries - including the contents of a
4246
* one-level container or map type.
43-
* <p>The type headers can be classes or fully-qualified class names.
47+
* <p> The type headers can be classes or fully-qualified class names.
48+
* <p> Since version 5.2.6, a SpEL expression option is provided to let to build a target
49+
*{@link ResolvableType} somehow externally.
4450
*
4551
* @author Mark Fisher
4652
* @author Artem Bilan
@@ -49,7 +55,7 @@
4955
*
5056
* @see JsonObjectMapper
5157
* @see org.springframework.integration.support.json.JsonObjectMapperProvider
52-
*
58+
* @see ResolvableType
5359
*/
5460
public class JsonToObjectTransformer extends AbstractTransformer implements BeanClassLoaderAware {
5561

@@ -59,6 +65,12 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
5965

6066
private ClassLoader classLoader;
6167

68+
private Expression valueTypeExpression =
69+
new FunctionExpression<Message<?>>((message) ->
70+
obtainResolvableTypeFromHeadersIfAny(message.getHeaders(), this.classLoader));
71+
72+
private EvaluationContext evaluationContext;
73+
6274
public JsonToObjectTransformer() {
6375
this((Class<?>) null);
6476
}
@@ -104,17 +116,52 @@ public void setBeanClassLoader(ClassLoader classLoader) {
104116
}
105117
}
106118

119+
/**
120+
* Configure a SpEL expression to evaluate a {@link ResolvableType}
121+
* to instantiate the payload from the incoming JSON.
122+
* By default this transformer consults {@link JsonHeaders} in the request message.
123+
* If this expression returns {@code null} or {@link ResolvableType} building throws a
124+
* {@link ClassNotFoundException}, this transformer falls back to the provided {@link #targetType}.
125+
* This logic is present as an expression because {@link JsonHeaders} may not have real class values,
126+
* but rather some type ids which have to be mapped to target classes according some external registry.
127+
* @param valueTypeExpressionString the SpEL expression to use.
128+
* @since 5.2.6
129+
*/
130+
public void setValueTypeExpressionString(String valueTypeExpressionString) {
131+
setValueTypeExpression(EXPRESSION_PARSER.parseExpression(valueTypeExpressionString));
132+
}
133+
134+
/**
135+
* Configure a SpEL {@link Expression} to evaluate a {@link ResolvableType}
136+
* to instantiate the payload from the incoming JSON.
137+
* By default this transformer consults {@link JsonHeaders} in the request message.
138+
* If this expression returns {@code null} or {@link ResolvableType} building throws a
139+
* {@link ClassNotFoundException}, this transformer falls back to the provided {@link #targetType}.
140+
* This logic is present as an expression because {@link JsonHeaders} may not have real class values,
141+
* but rather some type ids which have to be mapped to target classes according some external registry.
142+
* @param valueTypeExpression the SpEL {@link Expression} to use.
143+
* @since 5.2.6
144+
*/
145+
public void setValueTypeExpression(Expression valueTypeExpression) {
146+
this.valueTypeExpression = valueTypeExpression;
147+
}
148+
107149
@Override
108150
public String getComponentType() {
109151
return "json-to-object-transformer";
110152
}
111153

154+
@Override
155+
protected void onInit() {
156+
super.onInit();
157+
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
158+
}
159+
112160
@Override
113161
protected Object doTransform(Message<?> message) {
114-
MessageHeaders headers = message.getHeaders();
115-
boolean removeHeaders = false;
116-
ResolvableType valueType = obtainResolvableTypeFromHeadersIfAny(headers);
162+
ResolvableType valueType = obtainResolvableType(message);
117163

164+
boolean removeHeaders = false;
118165
if (valueType != null) {
119166
removeHeaders = true;
120167
}
@@ -134,7 +181,7 @@ protected Object doTransform(Message<?> message) {
134181
if (removeHeaders) {
135182
return getMessageBuilderFactory()
136183
.withPayload(result)
137-
.copyHeaders(headers)
184+
.copyHeaders(message.getHeaders())
138185
.removeHeaders(JsonHeaders.HEADERS.toArray(new String[0]))
139186
.build();
140187
}
@@ -144,12 +191,31 @@ protected Object doTransform(Message<?> message) {
144191
}
145192

146193
@Nullable
147-
private ResolvableType obtainResolvableTypeFromHeadersIfAny(MessageHeaders headers) {
194+
private ResolvableType obtainResolvableType(Message<?> message) {
195+
try {
196+
return this.valueTypeExpression.getValue(this.evaluationContext, message, ResolvableType.class);
197+
}
198+
catch (Exception ex) {
199+
if (ex.getCause() instanceof ClassNotFoundException) {
200+
logger.info("Cannot build a ResolvableType from the request message '" + message +
201+
"' evaluating expression '" + this.valueTypeExpression.getExpressionString() + "'", ex);
202+
return null;
203+
}
204+
else {
205+
throw ex;
206+
}
207+
}
208+
}
209+
210+
@Nullable
211+
private static ResolvableType obtainResolvableTypeFromHeadersIfAny(MessageHeaders headers,
212+
ClassLoader classLoader) {
213+
148214
Object valueType = headers.get(JsonHeaders.RESOLVABLE_TYPE);
149215
Object typeIdHeader = headers.get(JsonHeaders.TYPE_ID);
150216
if (!(valueType instanceof ResolvableType) && typeIdHeader != null) {
151217
valueType =
152-
JsonHeaders.buildResolvableType(this.classLoader, typeIdHeader,
218+
JsonHeaders.buildResolvableType(classLoader, typeIdHeader,
153219
headers.get(JsonHeaders.CONTENT_TYPE_ID), headers.get(JsonHeaders.KEY_TYPE_ID));
154220
}
155221
return valueType instanceof ResolvableType

spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2683,12 +2683,12 @@
26832683
</xsd:choice>
26842684
<xsd:attribute name="type" type="xsd:string">
26852685
<xsd:annotation>
2686-
<xsd:documentation source="java:java.lang.Class"><![CDATA[
2686+
<xsd:documentation source="java:java.lang.Class">
26872687
Fully qualified name of the java type to be created by this transformer (e.g. foo.bar.Foo)
2688-
]]></xsd:documentation>
2688+
</xsd:documentation>
26892689
</xsd:annotation>
26902690
</xsd:attribute>
2691-
<xsd:attribute name="object-mapper" use="optional">
2691+
<xsd:attribute name="object-mapper">
26922692
<xsd:annotation>
26932693
<xsd:documentation>
26942694
Optional reference to a JsonObjectMapper instance.
@@ -2702,6 +2702,16 @@
27022702
</xsd:annotation>
27032703
</xsd:attribute>
27042704
<xsd:attribute name="id" type="xsd:string"/>
2705+
<xsd:attribute name="value-type-expression" type="xsd:string">
2706+
<xsd:annotation>
2707+
<xsd:documentation>
2708+
The SpEL expression to build a 'ResolvableType' for the payload to convert from incoming JSON.
2709+
By default this transformer consults 'JsonHeaders' in the request message.
2710+
If this expression returns null or throws 'ClassNotFoundException', the transformer falls back to
2711+
the configured 'type'.
2712+
</xsd:documentation>
2713+
</xsd:annotation>
2714+
</xsd:attribute>
27052715
</xsd:complexType>
27062716

27072717
<xsd:element name="payload-serializing-transformer">

spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
https://www.springframework.org/schema/integration/spring-integration.xsd">
99

1010
<json-to-object-transformer id="defaultJacksonMapperTransformer" input-channel="defaultObjectMapperInput"
11-
type="org.springframework.integration.json.TestPerson"/>
11+
type="org.springframework.integration.json.TestPerson"
12+
value-type-expression="T (Class).forName('non.existing.type')"/>
1213

1314
<json-to-object-transformer id="customJsonMapperTransformer" input-channel="customJsonObjectMapperInput"
1415
type="org.springframework.integration.json.TestPerson"

spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java

Lines changed: 21 additions & 8 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.
@@ -17,10 +17,15 @@
1717
package org.springframework.integration.json;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.Mockito.spy;
22+
import static org.mockito.Mockito.verify;
2023

21-
import org.junit.Test;
22-
import org.junit.runner.RunWith;
24+
import org.apache.commons.logging.Log;
25+
import org.junit.jupiter.api.Test;
26+
import org.mockito.ArgumentCaptor;
2327

28+
import org.springframework.beans.DirectFieldAccessor;
2429
import org.springframework.beans.factory.annotation.Autowired;
2530
import org.springframework.beans.factory.annotation.Qualifier;
2631
import org.springframework.core.ResolvableType;
@@ -32,17 +37,15 @@
3237
import org.springframework.messaging.Message;
3338
import org.springframework.messaging.MessageChannel;
3439
import org.springframework.messaging.MessageHandler;
35-
import org.springframework.test.context.ContextConfiguration;
36-
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
40+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3741

3842
/**
3943
* @author Mark Fisher
4044
* @author Artem Bilan
4145
*
4246
* @since 2.0
4347
*/
44-
@ContextConfiguration
45-
@RunWith(SpringJUnit4ClassRunner.class)
48+
@SpringJUnitConfig
4649
public class JsonToObjectTransformerParserTests {
4750

4851
@Autowired
@@ -63,12 +66,16 @@ public class JsonToObjectTransformerParserTests {
6366
private JsonObjectMapper<?, ?> jsonObjectMapper;
6467

6568
@Test
66-
public void defaultObjectMapper() {
69+
public void testDefaultObjectMapper() {
6770
Object jsonToObjectTransformer =
6871
TestUtils.getPropertyValue(this.defaultJacksonMapperTransformer, "transformer");
6972
assertThat(TestUtils.getPropertyValue(jsonToObjectTransformer, "jsonObjectMapper").getClass())
7073
.isEqualTo(Jackson2JsonObjectMapper.class);
7174

75+
DirectFieldAccessor dfa = new DirectFieldAccessor(jsonToObjectTransformer);
76+
Log logger = (Log) spy(dfa.getPropertyValue("logger"));
77+
dfa.setPropertyValue("logger", logger);
78+
7279
String jsonString =
7380
"{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":42," +
7481
"\"address\":{\"number\":123,\"street\":\"Main Street\"}}";
@@ -84,6 +91,12 @@ public void defaultObjectMapper() {
8491
assertThat(person.getLastName()).isEqualTo("Doe");
8592
assertThat(person.getAge()).isEqualTo(42);
8693
assertThat(person.getAddress().toString()).isEqualTo("123 Main Street");
94+
95+
ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
96+
verify(logger).info(stringArgumentCaptor.capture(), any(Exception.class));
97+
String logMessage = stringArgumentCaptor.getValue();
98+
99+
assertThat(logMessage).startsWith("Cannot build a ResolvableType from the request message");
87100
}
88101

89102
@Test

spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java

Lines changed: 5 additions & 4 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.
@@ -20,10 +20,11 @@
2020

2121
import java.util.List;
2222

23-
import org.junit.Test;
23+
import org.junit.jupiter.api.Test;
2424

2525
import org.springframework.core.ParameterizedTypeReference;
2626
import org.springframework.core.ResolvableType;
27+
import org.springframework.integration.expression.ValueExpression;
2728
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
2829
import org.springframework.messaging.Message;
2930
import org.springframework.messaging.support.GenericMessage;
@@ -72,8 +73,8 @@ public void objectPayloadWithCustomMapper() {
7273
ObjectMapper customMapper = new ObjectMapper();
7374
customMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, Boolean.TRUE);
7475
customMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, Boolean.TRUE);
75-
JsonToObjectTransformer transformer =
76-
new JsonToObjectTransformer(TestPerson.class, new Jackson2JsonObjectMapper(customMapper));
76+
JsonToObjectTransformer transformer = new JsonToObjectTransformer(new Jackson2JsonObjectMapper(customMapper));
77+
transformer.setValueTypeExpression(new ValueExpression<>(ResolvableType.forClass(TestPerson.class)));
7778
String jsonString = "{firstName:'John', lastName:'Doe', age:42, address:{number:123, street:'Main Street'}}";
7879
Message<?> message = transformer.transform(new GenericMessage<>(jsonString));
7980
TestPerson person = (TestPerson) message.getPayload();

src/reference/asciidoc/transformer.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ See <<json-transformers>> for more information about `JsonObjectMapper` implemen
277277

278278
The `StreamTransformer` transforms `InputStream` payloads to a `byte[]`( or a `String` if a `charset` is provided).
279279

280-
The following example shows how to use the `stream-tansformer` element in XML:
280+
The following example shows how to use the `stream-transformer` element in XML:
281281

282282
====
283283
[source, xml]
@@ -449,6 +449,11 @@ Starting with version 5.2, the `JsonToObjectTransformer` can be configured with
449449
Also this component now consults request message headers first for the presence of the `JsonHeaders.RESOLVABLE_TYPE` or `JsonHeaders.TYPE_ID` and falls back to the configured type otherwise.
450450
The `ObjectToJsonTransformer` now also populates a `JsonHeaders.RESOLVABLE_TYPE` header based on the request message payload for any possible downstream scenarios.
451451

452+
Starting with version 5.2.6, the `JsonToObjectTransformer` can be supplied with a `valueTypeExpression` to resolve a `ResolvableType` for the payload to convert from JSON at runtime against the request message.
453+
By default it consults `JsonHeaders` in the request message.
454+
If this expression returns `null` or `ResolvableType` building throws a `ClassNotFoundException`, the transformer falls back to the provided `targetType`.
455+
This logic is present as an expression because `JsonHeaders` may not have real class values, but rather some type ids which have to be mapped to target classes according some external registry.
456+
452457
[[Avro-transformers]]
453458
===== Apache Avro Transformers
454459

0 commit comments

Comments
 (0)