Skip to content

Commit dc5128b

Browse files
committed
GH-828 Add support for configuring additional routers
Resolves #828
1 parent b02fe24 commit dc5128b

File tree

3 files changed

+157
-19
lines changed

3 files changed

+157
-19
lines changed

docs/src/main/asciidoc/spring-cloud-function.adoc

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public class RoutingFunction implements Function<Object, Object> {
145145
The routing instructions could be communicated in several ways. We support providing instructions via Message headers, System
146146
properties as well as pluggable strategy. So let's look at some of the details
147147

148-
*MessageRoutingCallback*
148+
==== MessageRoutingCallback
149149

150150
The `MessageRoutingCallback` is a strategy to assist with determining the name of the route-to function definition.
151151

@@ -231,7 +231,7 @@ conflict resolutions in the event multiple mechanisms are used at the same time,
231231
3. Application Properties (Any function)
232232

233233

234-
*Function Filtering*
234+
==== Function Filtering
235235
Filtering is the type of routing where there are only two paths - 'go' or 'discard'. In terms of functions it mean
236236
you only want to invoke a certain function if some condition returns 'true', otherwise you want to discard input.
237237
However, when it comes to discarding input there are many interpretation of what it could mean in the context of your application.
@@ -261,6 +261,58 @@ due to the nature of the reactive functions which are invoked only once to pass
261261
is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual
262262
values (e.g., Message).
263263

264+
==== Multiple Routers
265+
266+
By default the framework will always have a single routing function configured as described in previous sections. However, there are times when you may need more then one routing function.
267+
In that case you can create your own instance of the `RoutingFunction` bean in addition to the existing one as long as you give it a name other than `functionRouter`.
268+
269+
You can pass `spring.cloud.function.routing-expression` or `spring.cloud.function.definition` to RoutinFunction as key/value pairs in the map.
270+
271+
Here is a simple example
272+
273+
----
274+
@Configuration
275+
protected static class MultipleRouterConfiguration {
276+
277+
@Bean
278+
RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) {
279+
Map<String, String> propertiesMap = new HashMap<>();
280+
propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'");
281+
return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback);
282+
}
283+
284+
@Bean
285+
public Function<String, String> reverse() {
286+
return v -> new StringBuilder(v).reverse().toString();
287+
}
288+
289+
@Bean
290+
public Function<String, String> uppercase() {
291+
return String::toUpperCase;
292+
}
293+
}
294+
----
295+
296+
and a test that demonstrates how it works
297+
298+
`
299+
----
300+
@Test
301+
public void testMultipleRouters() {
302+
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'");
303+
FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class);
304+
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
305+
assertThat(function).isNotNull();
306+
Message<String> message = MessageBuilder.withPayload("hello").build();
307+
assertThat(function.apply(message)).isEqualTo("HELLO");
308+
309+
function = functionCatalog.lookup("mySpecialRouter");
310+
assertThat(function).isNotNull();
311+
message = MessageBuilder.withPayload("hello").build();
312+
assertThat(function.apply(message)).isEqualTo("olleh");
313+
}
314+
----
315+
264316
=== Input/Output Enrichment
265317

266318
There are often times when you need to modify or refine an incoming or outgoing Message and to keep your code clean of non-functional concerns. You don’t want to do it inside of your business logic.

spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.cloud.function.context.config;
1818

19+
import java.util.Map;
1920
import java.util.function.Function;
2021

2122
import org.apache.commons.logging.Log;
@@ -34,12 +35,13 @@
3435
import org.springframework.expression.BeanResolver;
3536
import org.springframework.expression.Expression;
3637
import org.springframework.expression.spel.standard.SpelExpressionParser;
38+
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
39+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
3740
import org.springframework.expression.spel.support.StandardEvaluationContext;
3841
import org.springframework.messaging.Message;
3942
import org.springframework.util.Assert;
4043
import org.springframework.util.StringUtils;
4144

42-
4345
/**
4446
* An implementation of Function which acts as a gateway/router by actually
4547
* delegating incoming invocation to a function specified .. .
@@ -60,6 +62,9 @@ public class RoutingFunction implements Function<Object, Object> {
6062

6163
private final StandardEvaluationContext evalContext = new StandardEvaluationContext();
6264

65+
private final SimpleEvaluationContext headerEvalContext = SimpleEvaluationContext
66+
.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess()).build();
67+
6368
private final SpelExpressionParser spelParser = new SpelExpressionParser();
6469

6570
private final FunctionCatalog functionCatalog;
@@ -72,6 +77,18 @@ public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties funct
7277
this(functionCatalog, functionProperties, null, null);
7378
}
7479

80+
public RoutingFunction(FunctionCatalog functionCatalog, Map<String, String> propertiesMap,
81+
BeanResolver beanResolver, MessageRoutingCallback routingCallback) {
82+
this(functionCatalog, extractIntoFunctionProperties(propertiesMap), beanResolver, routingCallback);
83+
}
84+
85+
private static FunctionProperties extractIntoFunctionProperties(Map<String, String> propertiesMap) {
86+
FunctionProperties functionProperties = new FunctionProperties();
87+
functionProperties.setDefinition(propertiesMap.get(FunctionProperties.FUNCTION_DEFINITION));
88+
functionProperties.setRoutingExpression(propertiesMap.get(FunctionProperties.PREFIX + ".routing-expression"));
89+
return functionProperties;
90+
}
91+
7592
public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties functionProperties,
7693
BeanResolver beanResolver, MessageRoutingCallback routingCallback) {
7794
this.functionCatalog = functionCatalog;
@@ -124,7 +141,7 @@ private Object route(Object input, boolean originalInputIsPublisher) {
124141
}
125142
}
126143
else if (StringUtils.hasText((String) message.getHeaders().get("spring.cloud.function.routing-expression"))) {
127-
function = this.functionFromExpression((String) message.getHeaders().get("spring.cloud.function.routing-expression"), message);
144+
function = this.functionFromExpression((String) message.getHeaders().get("spring.cloud.function.routing-expression"), message, true);
128145
if (function.isInputTypePublisher()) {
129146
this.assertOriginalInputIsNotPublisher(originalInputIsPublisher);
130147
}
@@ -193,12 +210,16 @@ private FunctionInvocationWrapper functionFromDefinition(String definition) {
193210
}
194211

195212
private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input) {
213+
return functionFromExpression(routingExpression, input, false);
214+
}
215+
216+
private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input, boolean isViaHeader) {
196217
Expression expression = spelParser.parseExpression(routingExpression);
197218
if (input instanceof Message) {
198219
input = MessageUtils.toCaseInsensitiveHeadersStructure((Message<?>) input);
199220
}
200221

201-
String functionName = expression.getValue(this.evalContext, input, String.class);
222+
String functionName = isViaHeader ? expression.getValue(this.headerEvalContext, input, String.class) : expression.getValue(this.evalContext, input, String.class);
202223
Assert.hasText(functionName, "Failed to resolve function name based on routing expression '" + functionProperties.getRoutingExpression() + "'");
203224
FunctionInvocationWrapper function = functionCatalog.lookup(functionName);
204225
Assert.notNull(function, "Failed to lookup function to route to based on the expression '"

spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cloud.function.context.config;
1818

19+
import java.util.HashMap;
20+
import java.util.Map;
1921
import java.util.function.Function;
2022

2123
import org.junit.jupiter.api.AfterEach;
@@ -24,17 +26,22 @@
2426
import reactor.core.publisher.Flux;
2527
import reactor.test.StepVerifier;
2628

29+
import org.springframework.beans.factory.BeanFactory;
2730
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2831
import org.springframework.boot.builder.SpringApplicationBuilder;
2932
import org.springframework.cloud.function.context.FunctionCatalog;
3033
import org.springframework.cloud.function.context.FunctionProperties;
34+
import org.springframework.cloud.function.context.MessageRoutingCallback;
3135
import org.springframework.context.ConfigurableApplicationContext;
3236
import org.springframework.context.annotation.Bean;
3337
import org.springframework.context.annotation.Configuration;
38+
import org.springframework.context.expression.BeanFactoryResolver;
39+
import org.springframework.lang.Nullable;
3440
import org.springframework.messaging.Message;
3541
import org.springframework.messaging.support.MessageBuilder;
3642

3743
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.junit.Assert.fail;
3845

3946
/**
4047
*
@@ -52,13 +59,17 @@ public void before() {
5259
context.close();
5360
}
5461

55-
private FunctionCatalog configureCatalog() {
56-
context = new SpringApplicationBuilder(RoutingFunctionConfiguration.class).run(
62+
private FunctionCatalog configureCatalog(Class<?> configurationClass) {
63+
context = new SpringApplicationBuilder(configurationClass).run(
5764
"--logging.level.org.springframework.cloud.function=DEBUG",
5865
"--spring.cloud.function.routing.enabled=true");
5966
return context.getBean(FunctionCatalog.class);
6067
}
6168

69+
private FunctionCatalog configureCatalog() {
70+
return configureCatalog(RoutingFunctionConfiguration.class);
71+
}
72+
6273
@SuppressWarnings({ "unchecked", "rawtypes" })
6374
@Test
6475
public void testInvocationWithMessageAndHeader() {
@@ -91,10 +102,7 @@ public void testRoutingReactiveInputWithReactiveFunctionAndDefinitionMessageHead
91102
.setHeader(FunctionProperties.PREFIX + ".definition", "echoFlux").build();
92103
Flux resultFlux = (Flux) function.apply(Flux.just(message));
93104

94-
StepVerifier
95-
.create(resultFlux)
96-
.expectError()
97-
.verify();
105+
StepVerifier.create(resultFlux).expectError().verify();
98106
}
99107

100108
@SuppressWarnings({ "unchecked", "rawtypes" })
@@ -106,10 +114,27 @@ public void testRoutingReactiveInputWithReactiveFunctionAndExpressionMessageHead
106114
Message<String> message = MessageBuilder.withPayload("hello")
107115
.setHeader(FunctionProperties.PREFIX + ".routing-expression", "'echoFlux'").build();
108116
Flux resultFlux = (Flux) function.apply(Flux.just(message));
109-
StepVerifier
110-
.create(resultFlux)
111-
.expectError()
112-
.verify();
117+
StepVerifier.create(resultFlux).expectError().verify();
118+
}
119+
120+
@SuppressWarnings({ "unchecked", "rawtypes" })
121+
@Test
122+
public void failWithHeaderProvidedExpressionAccessingRuntime() {
123+
FunctionCatalog functionCatalog = this.configureCatalog();
124+
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
125+
assertThat(function).isNotNull();
126+
Message<String> message = MessageBuilder.withPayload("hello")
127+
.setHeader(FunctionProperties.PREFIX + ".routing-expression",
128+
"T(java.lang.Runtime).getRuntime().exec(\"open -a calculator.app\")")
129+
.build();
130+
try {
131+
function.apply(message);
132+
fail();
133+
}
134+
catch (Exception e) {
135+
assertThat(e.getMessage()).isEqualTo("EL1005E: Type cannot be found 'java.lang.Runtime'");
136+
}
137+
113138
}
114139

115140
@SuppressWarnings({ "unchecked", "rawtypes" })
@@ -151,7 +176,8 @@ public void testInvocationWithMessageAndRoutingExpressionCaseInsensitive() {
151176
@SuppressWarnings({ "rawtypes", "unchecked" })
152177
@Test
153178
public void testInvocationWithRoutingBeanExpression() {
154-
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "@reverse.apply(#root.getHeaders().get('func'))");
179+
System.setProperty(FunctionProperties.PREFIX + ".routing-expression",
180+
"@reverse.apply(#root.getHeaders().get('func'))");
155181
FunctionCatalog functionCatalog = this.configureCatalog();
156182
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
157183
assertThat(function).isNotNull();
@@ -170,16 +196,17 @@ public void testOtherExpectedFailures() {
170196
Assertions.fail();
171197
}
172198
catch (Exception e) {
173-
//ignore
199+
// ignore
174200
}
175201

176202
// non existing function
177203
try {
178-
function.apply(MessageBuilder.withPayload("hello").setHeader(FunctionProperties.PREFIX + ".definition", "blah").build());
204+
function.apply(MessageBuilder.withPayload("hello")
205+
.setHeader(FunctionProperties.PREFIX + ".definition", "blah").build());
179206
Assertions.fail();
180207
}
181208
catch (Exception e) {
182-
//ignore
209+
// ignore
183210
}
184211
}
185212

@@ -197,6 +224,22 @@ public void testInvocationWithMessageComposed() {
197224
assertThat(function.apply(message)).isEqualTo("OLLEH");
198225
}
199226

227+
@SuppressWarnings({ "rawtypes", "unchecked" })
228+
@Test
229+
public void testMultipleRouters() {
230+
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'");
231+
FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class);
232+
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
233+
assertThat(function).isNotNull();
234+
Message<String> message = MessageBuilder.withPayload("hello").build();
235+
assertThat(function.apply(message)).isEqualTo("HELLO");
236+
237+
function = functionCatalog.lookup("mySpecialRouter");
238+
assertThat(function).isNotNull();
239+
message = MessageBuilder.withPayload("hello").build();
240+
assertThat(function.apply(message)).isEqualTo("olleh");
241+
}
242+
200243
@EnableAutoConfiguration
201244
@Configuration
202245
protected static class RoutingFunctionConfiguration {
@@ -216,4 +259,26 @@ public Function<Flux<String>, Flux<String>> echoFlux() {
216259
return f -> f;
217260
}
218261
}
262+
263+
@EnableAutoConfiguration
264+
@Configuration
265+
protected static class MultipleRouterConfiguration {
266+
267+
@Bean
268+
RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) {
269+
Map<String, String> propertiesMap = new HashMap<>();
270+
propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'");
271+
return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback);
272+
}
273+
274+
@Bean
275+
public Function<String, String> reverse() {
276+
return v -> new StringBuilder(v).reverse().toString();
277+
}
278+
279+
@Bean
280+
public Function<String, String> uppercase() {
281+
return String::toUpperCase;
282+
}
283+
}
219284
}

0 commit comments

Comments
 (0)