Fix JsonToObjectTransformer for ClassNotFoundEx (#3228)

* Fix JsonToObjectTransformer for ClassNotFoundEx

Related to https://github.com/spring-projects/spring-integration/issues/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
This commit is contained in:
Artem Bilan
2020-03-27 15:42:24 -04:00
committed by GitHub
parent f1a0a066af
commit 9c46b3420e
7 changed files with 129 additions and 28 deletions

View File

@@ -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.
@@ -26,6 +26,7 @@ import org.springframework.util.StringUtils;
/**
* @author Mark Fisher
* @author Artem Bilan
*
* @since 2.0
*/
public class JsonToObjectTransformerParser extends AbstractTransformerParser {
@@ -45,6 +46,10 @@ public class JsonToObjectTransformerParser extends AbstractTransformerParser {
if (StringUtils.hasText(objectMapper)) {
builder.addConstructorArgReference(objectMapper);
}
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "value-type-expression",
"valueTypeExpressionString");
}
}

View File

@@ -21,6 +21,10 @@ import java.io.UncheckedIOException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.core.ResolvableType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.integration.support.json.JsonObjectMapper;
import org.springframework.integration.support.json.JsonObjectMapperProvider;
@@ -37,10 +41,12 @@ import org.springframework.util.Assert;
* factory to get an instance of Jackson JSON-processor
* if jackson-databind lib is present on the classpath. Any other {@linkplain JsonObjectMapper}
* implementation can be provided.
* <p>Since version 3.0, you can omit the target class and the target type can be
* <p> Since version 3.0, you can omit the target class and the target type can be
* determined by the {@link JsonHeaders} type entries - including the contents of a
* one-level container or map type.
* <p>The type headers can be classes or fully-qualified class names.
* <p> The type headers can be classes or fully-qualified class names.
* <p> Since version 5.2.6, a SpEL expression option is provided to let to build a target
*{@link ResolvableType} somehow externally.
*
* @author Mark Fisher
* @author Artem Bilan
@@ -49,7 +55,7 @@ import org.springframework.util.Assert;
*
* @see JsonObjectMapper
* @see org.springframework.integration.support.json.JsonObjectMapperProvider
*
* @see ResolvableType
*/
public class JsonToObjectTransformer extends AbstractTransformer implements BeanClassLoaderAware {
@@ -59,6 +65,12 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
private ClassLoader classLoader;
private Expression valueTypeExpression =
new FunctionExpression<Message<?>>((message) ->
obtainResolvableTypeFromHeadersIfAny(message.getHeaders(), this.classLoader));
private EvaluationContext evaluationContext;
public JsonToObjectTransformer() {
this((Class<?>) null);
}
@@ -104,17 +116,52 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
}
}
/**
* Configure a SpEL expression to evaluate a {@link ResolvableType}
* to instantiate the payload from the incoming JSON.
* By default this transformer consults {@link JsonHeaders} in the request message.
* If this expression returns {@code null} or {@link ResolvableType} building throws a
* {@link ClassNotFoundException}, this transformer falls back to the provided {@link #targetType}.
* This logic is present as an expression because {@link JsonHeaders} may not have real class values,
* but rather some type ids which have to be mapped to target classes according some external registry.
* @param valueTypeExpressionString the SpEL expression to use.
* @since 5.2.6
*/
public void setValueTypeExpressionString(String valueTypeExpressionString) {
setValueTypeExpression(EXPRESSION_PARSER.parseExpression(valueTypeExpressionString));
}
/**
* Configure a SpEL {@link Expression} to evaluate a {@link ResolvableType}
* to instantiate the payload from the incoming JSON.
* By default this transformer consults {@link JsonHeaders} in the request message.
* If this expression returns {@code null} or {@link ResolvableType} building throws a
* {@link ClassNotFoundException}, this transformer falls back to the provided {@link #targetType}.
* This logic is present as an expression because {@link JsonHeaders} may not have real class values,
* but rather some type ids which have to be mapped to target classes according some external registry.
* @param valueTypeExpression the SpEL {@link Expression} to use.
* @since 5.2.6
*/
public void setValueTypeExpression(Expression valueTypeExpression) {
this.valueTypeExpression = valueTypeExpression;
}
@Override
public String getComponentType() {
return "json-to-object-transformer";
}
@Override
protected Object doTransform(Message<?> message) {
MessageHeaders headers = message.getHeaders();
boolean removeHeaders = false;
ResolvableType valueType = obtainResolvableTypeFromHeadersIfAny(headers);
protected void onInit() {
super.onInit();
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
}
@Override
protected Object doTransform(Message<?> message) {
ResolvableType valueType = obtainResolvableType(message);
boolean removeHeaders = false;
if (valueType != null) {
removeHeaders = true;
}
@@ -134,7 +181,7 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
if (removeHeaders) {
return getMessageBuilderFactory()
.withPayload(result)
.copyHeaders(headers)
.copyHeaders(message.getHeaders())
.removeHeaders(JsonHeaders.HEADERS.toArray(new String[0]))
.build();
}
@@ -144,12 +191,31 @@ public class JsonToObjectTransformer extends AbstractTransformer implements Bean
}
@Nullable
private ResolvableType obtainResolvableTypeFromHeadersIfAny(MessageHeaders headers) {
private ResolvableType obtainResolvableType(Message<?> message) {
try {
return this.valueTypeExpression.getValue(this.evaluationContext, message, ResolvableType.class);
}
catch (Exception ex) {
if (ex.getCause() instanceof ClassNotFoundException) {
logger.info("Cannot build a ResolvableType from the request message '" + message +
"' evaluating expression '" + this.valueTypeExpression.getExpressionString() + "'", ex);
return null;
}
else {
throw ex;
}
}
}
@Nullable
private static ResolvableType obtainResolvableTypeFromHeadersIfAny(MessageHeaders headers,
ClassLoader classLoader) {
Object valueType = headers.get(JsonHeaders.RESOLVABLE_TYPE);
Object typeIdHeader = headers.get(JsonHeaders.TYPE_ID);
if (!(valueType instanceof ResolvableType) && typeIdHeader != null) {
valueType =
JsonHeaders.buildResolvableType(this.classLoader, typeIdHeader,
JsonHeaders.buildResolvableType(classLoader, typeIdHeader,
headers.get(JsonHeaders.CONTENT_TYPE_ID), headers.get(JsonHeaders.KEY_TYPE_ID));
}
return valueType instanceof ResolvableType

View File

@@ -2683,12 +2683,12 @@
</xsd:choice>
<xsd:attribute name="type" type="xsd:string">
<xsd:annotation>
<xsd:documentation source="java:java.lang.Class"><![CDATA[
<xsd:documentation source="java:java.lang.Class">
Fully qualified name of the java type to be created by this transformer (e.g. foo.bar.Foo)
]]></xsd:documentation>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="object-mapper" use="optional">
<xsd:attribute name="object-mapper">
<xsd:annotation>
<xsd:documentation>
Optional reference to a JsonObjectMapper instance.
@@ -2702,6 +2702,16 @@
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="id" type="xsd:string"/>
<xsd:attribute name="value-type-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation>
The SpEL expression to build a 'ResolvableType' for the payload to convert from incoming JSON.
By default this transformer consults 'JsonHeaders' in the request message.
If this expression returns null or throws 'ClassNotFoundException', the transformer falls back to
the configured 'type'.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:element name="payload-serializing-transformer">

View File

@@ -8,7 +8,8 @@
https://www.springframework.org/schema/integration/spring-integration.xsd">
<json-to-object-transformer id="defaultJacksonMapperTransformer" input-channel="defaultObjectMapperInput"
type="org.springframework.integration.json.TestPerson"/>
type="org.springframework.integration.json.TestPerson"
value-type-expression="T (Class).forName('non.existing.type')"/>
<json-to-object-transformer id="customJsonMapperTransformer" input-channel="customJsonObjectMapperInput"
type="org.springframework.integration.json.TestPerson"

View File

@@ -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.
@@ -17,10 +17,15 @@
package org.springframework.integration.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ResolvableType;
@@ -32,8 +37,7 @@ import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Mark Fisher
@@ -41,8 +45,7 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
*
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
public class JsonToObjectTransformerParserTests {
@Autowired
@@ -63,12 +66,16 @@ public class JsonToObjectTransformerParserTests {
private JsonObjectMapper<?, ?> jsonObjectMapper;
@Test
public void defaultObjectMapper() {
public void testDefaultObjectMapper() {
Object jsonToObjectTransformer =
TestUtils.getPropertyValue(this.defaultJacksonMapperTransformer, "transformer");
assertThat(TestUtils.getPropertyValue(jsonToObjectTransformer, "jsonObjectMapper").getClass())
.isEqualTo(Jackson2JsonObjectMapper.class);
DirectFieldAccessor dfa = new DirectFieldAccessor(jsonToObjectTransformer);
Log logger = (Log) spy(dfa.getPropertyValue("logger"));
dfa.setPropertyValue("logger", logger);
String jsonString =
"{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":42," +
"\"address\":{\"number\":123,\"street\":\"Main Street\"}}";
@@ -84,6 +91,12 @@ public class JsonToObjectTransformerParserTests {
assertThat(person.getLastName()).isEqualTo("Doe");
assertThat(person.getAge()).isEqualTo(42);
assertThat(person.getAddress().toString()).isEqualTo("123 Main Street");
ArgumentCaptor<String> stringArgumentCaptor = ArgumentCaptor.forClass(String.class);
verify(logger).info(stringArgumentCaptor.capture(), any(Exception.class));
String logMessage = stringArgumentCaptor.getValue();
assertThat(logMessage).startsWith("Cannot build a ResolvableType from the request message");
}
@Test

View File

@@ -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.
@@ -20,10 +20,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
@@ -72,8 +73,8 @@ public class JsonToObjectTransformerTests {
ObjectMapper customMapper = new ObjectMapper();
customMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, Boolean.TRUE);
customMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, Boolean.TRUE);
JsonToObjectTransformer transformer =
new JsonToObjectTransformer(TestPerson.class, new Jackson2JsonObjectMapper(customMapper));
JsonToObjectTransformer transformer = new JsonToObjectTransformer(new Jackson2JsonObjectMapper(customMapper));
transformer.setValueTypeExpression(new ValueExpression<>(ResolvableType.forClass(TestPerson.class)));
String jsonString = "{firstName:'John', lastName:'Doe', age:42, address:{number:123, street:'Main Street'}}";
Message<?> message = transformer.transform(new GenericMessage<>(jsonString));
TestPerson person = (TestPerson) message.getPayload();

View File

@@ -277,7 +277,7 @@ See <<json-transformers>> for more information about `JsonObjectMapper` implemen
The `StreamTransformer` transforms `InputStream` payloads to a `byte[]`( or a `String` if a `charset` is provided).
The following example shows how to use the `stream-tansformer` element in XML:
The following example shows how to use the `stream-transformer` element in XML:
====
[source, xml]
@@ -449,6 +449,11 @@ Starting with version 5.2, the `JsonToObjectTransformer` can be configured with
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.
The `ObjectToJsonTransformer` now also populates a `JsonHeaders.RESOLVABLE_TYPE` header based on the request message payload for any possible downstream scenarios.
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.
By default it consults `JsonHeaders` in the request message.
If this expression returns `null` or `ResolvableType` building throws a `ClassNotFoundException`, the transformer falls back to the provided `targetType`.
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.
[[Avro-transformers]]
===== Apache Avro Transformers