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:
@@ -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");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user