diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java
index 9f9c728c24..bc248c790c 100644
--- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java
+++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java
@@ -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");
+
}
}
diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java
index 463ca06329..90d351688f 100644
--- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java
+++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java
@@ -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.
- *
Since version 3.0, you can omit the target class and the target type can be
+ *
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.
- *
The type headers can be classes or fully-qualified class names.
+ *
The type headers can be classes or fully-qualified class names.
+ *
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) ->
+ 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
diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.2.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.2.xsd
index 6d28a8a53e..24820cfa09 100644
--- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.2.xsd
+++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.2.xsd
@@ -2647,12 +2647,12 @@
-
Fully qualified name of the java type to be created by this transformer (e.g. foo.bar.Foo)
- ]]>
+
-
+
Optional reference to a JsonObjectMapper instance.
@@ -2666,6 +2666,16 @@
+
+
+
+ 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'.
+
+
+
diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml
index 3b70d85e20..a5d3c5b8d2 100644
--- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml
+++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml
@@ -8,7 +8,8 @@
https://www.springframework.org/schema/integration/spring-integration.xsd">
+ type="org.springframework.integration.json.TestPerson"
+ value-type-expression="T (Class).forName('non.existing.type')"/>
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 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
diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java
index 48c2c07986..f2fae531f1 100644
--- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java
+++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java
@@ -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();
diff --git a/src/reference/asciidoc/transformer.adoc b/src/reference/asciidoc/transformer.adoc
index aaf411e256..59c17ee461 100644
--- a/src/reference/asciidoc/transformer.adoc
+++ b/src/reference/asciidoc/transformer.adoc
@@ -277,7 +277,7 @@ See <> 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]
@@ -453,6 +453,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