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.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd index a6cafa938a..c5243b4dd0 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration.xsd @@ -2683,12 +2683,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. @@ -2702,6 +2702,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 ea2a76b51b..fe1573605a 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] @@ -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