From 22e945009aa0f0eac15dee006dbe021b1d4d48e9 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 24 May 2021 15:18:28 +0200 Subject: [PATCH] GH-676 Relax SpEL evaluation failures for input header enrichment Add documentation Resolves #676 --- .../main/asciidoc/spring-cloud-function.adoc | 82 ++++++++++++++++--- .../context/catalog/InputEnricher.java | 21 ++++- ...ests.java => InputHeaderMappingTests.java} | 28 ++++++- 3 files changed, 117 insertions(+), 14 deletions(-) rename spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/{FunctionPropertiesTests.java => InputHeaderMappingTests.java} (85%) diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index 56dee3722..f130d7646 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -258,6 +258,78 @@ due to the nature of the reactive functions which are invoked only once to pass is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual values (e.g., Message). +=== Input Enrichment + +There are often times when you need to modify or refine an incoming Message and to keep your code clean of non-functional concerns, and you don’t want to +do it inside of your business logic. + +You can always accomplish it via <>. Such approach provides several benefits: + +- It allows you to isolate this non-functional concern into a separate function which you can compose with the business function as function definition. +- It provides you with complete freedom (and danger) as to what you can modify before incoming message reaches the actual business function. + +[source, java] +---- +@Bean +public Function, Message> enrich() { + return message -> MessageBuilder.fromMessage(message).setHeader("foo", "bar").build(); +} + +@Bean +public Function, Message> myBusinessFunction() { + // do whatever +} +---- + +And then compose your function by providing the following function definition `enrich|myBusinessFunction`. + +While the described approach is the most flexible, it is also the most involved as it requires you to write some code, make it a bean or +manually register it as a function before you can compose it with the business function as you can see from the preceding example. + +But what if modifications (enrichments) you are trying to make are trivial as they are in the preceding example? Is there a simpler and more dynamic and configurable + mechanism to accomplish the same? + +Since version 3.1.3, the framework allows you to provide SpEL expression to enrich individual message headers. Let’s look at one of the tests as the example. + +[source, java] +---- +@Test +public void testInputHeaderMappingPropertyWithoutIndex() throws Exception { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( + "--spring.cloud.function.configuration.echo.input-header-mapping-expression.key1='hello1'", + "--spring.cloud.function.configuration.echo.input-header-mapping-expression.key2='hello2'", + "--spring.cloud.function.configuration.echo.input-header-mapping-expression.foo=headers.contentType")) { + + FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class); + FunctionInvocationWrapper function = functionCatalog.lookup("echo"); + function.apply(MessageBuilder.withPayload("helo") + .setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build()); + } +} +---- + +Here you see a property called `input-header-mapping-expression` preceded by the name of the function (i.e., `echo`) and followed by the name of the +message header key you want to set and the value as SpEL expression. The first two expressions (for 'key1' and 'key2') are literal SpEL expressions enclosed in +single quotes, effectively setting 'key1' to value `hello1` and 'key2' to value `hello2`. The third one will map Message header ‘foo’ to the value of the +current ‘contentType’ header. + +NOTE: if for whatever reason the provided expression evaluation fails, the execution of the function will proceed as if nothing ever happen. +However you will see the WARN message in your logs informing you about it + +[source, text] +---- +o.s.c.f.context.catalog.InputEnricher : Failed while evaluating expression "hello1" on incoming message. . . +---- + +In the event you are dealing with functions that have multiple inputs (next section), you can use index immediately after `input-header-mapping-expression` + +[source, text] +---- +--spring.cloud.function.configuration.echo.input-header-mapping-expression[0].key1=‘hello1' +--spring.cloud.function.configuration.echo.input-header-mapping-expression[1].key2='hello2' +---- + === Function Arity There are times when a stream of data needs to be categorized and organized. For example, @@ -470,16 +542,6 @@ same rules for signature transformation outlined in "Java 8 function support" se To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate autoconfiguration and supporting classes. -=== Function Component Scan - -Spring Cloud Function will scan for implementations of `Function`, -`Consumer` and `Supplier` in a package called `functions` if it -exists. Using this feature you can write functions that have no -dependencies on Spring - not even the `@Component` annotation is -needed. If you want to use a different package, you can set -`spring.cloud.function.scan.packages`. You can also use -`spring.cloud.function.scan.enabled=false` to switch off the scan -completely. == Standalone Web Applications diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InputEnricher.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InputEnricher.java index 2cc95f411..76ce23d4a 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InputEnricher.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InputEnricher.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.function.Function; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; @@ -37,7 +39,9 @@ import org.springframework.util.Assert; * @since 3.1.3 * */ -public class InputEnricher implements Function { +class InputEnricher implements Function { + + protected Log logger = LogFactory.getLog(InputEnricher.class); private final Map> headerExpressions; @@ -62,8 +66,19 @@ public class InputEnricher implements Function { Map mappings = this.headerExpressions.get("0"); for (Entry keyValueExpressionEntry : mappings.entrySet()) { Expression expression = this.spelParser.parseExpression(keyValueExpressionEntry.getValue()); - Object value = expression.getValue(this.evalContext, input, Object.class); - messageBuilder.setHeader(keyValueExpressionEntry.getKey(), value); + try { + Object value = expression.getValue(this.evalContext, input, Object.class); + messageBuilder.setHeader(keyValueExpressionEntry.getKey(), value); + } + catch (Exception e) { + String message = "Failed while evaluating expression \"" + keyValueExpressionEntry.getValue() + "\" on incoming message"; + if (logger.isDebugEnabled()) { + logger.warn(message + ": " + input, e); + } + else { + logger.warn(message); + } + } } input = messageBuilder.build(); } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionPropertiesTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/InputHeaderMappingTests.java similarity index 85% rename from spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionPropertiesTests.java rename to spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/InputHeaderMappingTests.java index 9a5fea593..10893248f 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionPropertiesTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/InputHeaderMappingTests.java @@ -34,7 +34,23 @@ import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; //NOTE!!! assertions for all tests are in 'echo' function since we're validating what's coming into it. -public class FunctionPropertiesTests { +public class InputHeaderMappingTests { + + @Test + public void testErrorWarnAndContinue() throws Exception { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + SampleFunctionConfiguration.class).web(WebApplicationType.NONE).run( + "--logging.level.org.springframework.cloud.function=DEBUG", + "--spring.main.lazy-initialization=true", + "--spring.cloud.function.configuration.echoFail.input-header-mapping-expression[0].key1=hello1", + "--spring.cloud.function.configuration.echoFail.input-header-mapping-expression[0].key2='hello2'", + "--spring.cloud.function.configuration.echoFail.input-header-mapping-expression[0].foo=headers.contentType")) { + + FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class); + FunctionInvocationWrapper function = functionCatalog.lookup("echoFail"); + function.apply(MessageBuilder.withPayload("helo").build()); + } + } @Test public void testInputHeaderMappingPropertyWithIndex() throws Exception { @@ -137,6 +153,16 @@ public class FunctionPropertiesTests { }; } + @Bean + public Function, Message> echoFail() { + return m -> { + assertThat(m.getHeaders().containsKey("key1")).isFalse(); + assertThat(m.getHeaders().get("key2")).isEqualTo("hello2"); + assertThat(m.getHeaders().containsKey("foo")).isFalse(); + return m; + }; + } + @Bean public Function, Message> split() { return m -> {