GH-676 Relax SpEL evaluation failures for input header enrichment
Add documentation Resolves #676
This commit is contained in:
@@ -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
|
is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual
|
||||||
values (e.g., Message).
|
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 <<Function Composition>>. 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<?>, Message<?>> enrich() {
|
||||||
|
return message -> MessageBuilder.fromMessage(message).setHeader("foo", "bar").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Message<?>, 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
|
=== Function Arity
|
||||||
|
|
||||||
There are times when a stream of data needs to be categorized and organized. For example,
|
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
|
To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate
|
||||||
autoconfiguration and supporting classes.
|
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
|
== Standalone Web Applications
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import java.util.Map;
|
|||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.function.Function;
|
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.context.expression.MapAccessor;
|
||||||
import org.springframework.expression.BeanResolver;
|
import org.springframework.expression.BeanResolver;
|
||||||
import org.springframework.expression.Expression;
|
import org.springframework.expression.Expression;
|
||||||
@@ -37,7 +39,9 @@ import org.springframework.util.Assert;
|
|||||||
* @since 3.1.3
|
* @since 3.1.3
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class InputEnricher implements Function<Object, Object> {
|
class InputEnricher implements Function<Object, Object> {
|
||||||
|
|
||||||
|
protected Log logger = LogFactory.getLog(InputEnricher.class);
|
||||||
|
|
||||||
private final Map<String, Map<String, String>> headerExpressions;
|
private final Map<String, Map<String, String>> headerExpressions;
|
||||||
|
|
||||||
@@ -62,8 +66,19 @@ public class InputEnricher implements Function<Object, Object> {
|
|||||||
Map<String, String> mappings = this.headerExpressions.get("0");
|
Map<String, String> mappings = this.headerExpressions.get("0");
|
||||||
for (Entry<String, String> keyValueExpressionEntry : mappings.entrySet()) {
|
for (Entry<String, String> keyValueExpressionEntry : mappings.entrySet()) {
|
||||||
Expression expression = this.spelParser.parseExpression(keyValueExpressionEntry.getValue());
|
Expression expression = this.spelParser.parseExpression(keyValueExpressionEntry.getValue());
|
||||||
Object value = expression.getValue(this.evalContext, input, Object.class);
|
try {
|
||||||
messageBuilder.setHeader(keyValueExpressionEntry.getKey(), value);
|
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();
|
input = messageBuilder.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,23 @@ import org.springframework.messaging.support.MessageBuilder;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.
|
//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
|
@Test
|
||||||
public void testInputHeaderMappingPropertyWithIndex() throws Exception {
|
public void testInputHeaderMappingPropertyWithIndex() throws Exception {
|
||||||
@@ -137,6 +153,16 @@ public class FunctionPropertiesTests {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Message<?>, 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
|
@Bean
|
||||||
public Function<Message<?>, Message<?>> split() {
|
public Function<Message<?>, Message<?>> split() {
|
||||||
return m -> {
|
return m -> {
|
||||||
Reference in New Issue
Block a user