Skip to content

Commit e925795

Browse files
authored
GH-3946: Revise Router channelKeyFallback option (#3948)
* GH-3946: Revise Router channelKeyFallback option Fixes #3946 The `AbstractMappingMessageRouter` has both `resolutionRequired` and `channelKeyFallback` as `true` by default. End-users expects them to back off when they set a `defaultOutputChannel`. They really want something similar to Java `switch` statement * Change the logic in the `AbstractMappingMessageRouter` to reset `channelKeyFallback` to `false` when `defaultOutputChannel` to avoid attempts to resolve channel from name, but rather fallback to `defaultOutputChannel` as it states from th mentioned Java `switch` statement experience * Deprecate `RouterSpec.noChannelKeyFallback()` in favor of newly introduced `channelKeyFallback(boolean)` * Call `channelKeyFallback(false)` from an overloaded `defaultOutputToParentFlow()` to reflect the mentioned expected behavior in Java DSL as well. * Respectively, deprecate `KotlinRouterSpec.noChannelKeyFallback()` wrapper in favor of newly introduced `channelKeyFallback(channelKeyFallback: Boolean)` * Remove redundant already `noChannelKeyFallback()` option in the `NoFallbackAllowedTests` * Document the change and new behavior * Fix `IntegrationGraphServerTests` to `setChannelKeyFallback(true)` explicitly * Remove not relevant `default-output-channel` from the `DynamicRouterTests-context.xml` * Reject an `AbstractMappingMessageRouter` configuration where `defaultOutputChannel` is provided and both `channelKeyFallback` & `resolutionRequired` are set to `true`. Such a state makes `defaultOutputChannel` as not reachable and may cause some confusions in target applications. * Remove `&` symbol from JavaDocs * Fix `boolean` expression for `AbstractMappingMessageRouter` configuration check * Fix `IntegrationGraphServerTests` for new router behavior * Improve language in docs
1 parent 60ba544 commit e925795

File tree

8 files changed

+139
-28
lines changed

8 files changed

+139
-28
lines changed

spring-integration-core/src/main/java/org/springframework/integration/dsl/RouterSpec.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2021 the original author or authors.
2+
* Copyright 2016-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@
2323
import org.springframework.core.convert.support.DefaultConversionService;
2424
import org.springframework.integration.context.IntegrationObjectSupport;
2525
import org.springframework.integration.router.AbstractMappingMessageRouter;
26+
import org.springframework.integration.router.AbstractMessageRouter;
2627
import org.springframework.integration.support.context.NamedComponent;
2728
import org.springframework.integration.support.management.MappingMessageRouterManagement;
2829
import org.springframework.messaging.MessageChannel;
@@ -118,9 +119,24 @@ public RouterSpec<K, R> suffix(String suffix) {
118119
* cause a stack overflow.
119120
* @return the router spec.
120121
* @since 5.2
122+
* @deprecated since 6.0 in favor of {@link #channelKeyFallback(boolean)}
121123
*/
124+
@Deprecated(since = "6.0", forRemoval = true)
122125
public RouterSpec<K, R> noChannelKeyFallback() {
123-
this.handler.setChannelKeyFallback(false);
126+
return channelKeyFallback(false);
127+
}
128+
129+
/**
130+
* When true (default), if a resolved channel key does not exist in the channel map,
131+
* the key itself is used as the channel name, which we will attempt to resolve to a
132+
* channel. Set to {@code false} to disable this feature.
133+
* @param channelKeyFallback false to disable the fallback.
134+
* @return the router spec.
135+
* @since 6.0
136+
* @see AbstractMappingMessageRouter#setChannelKeyFallback(boolean)
137+
*/
138+
public RouterSpec<K, R> channelKeyFallback(boolean channelKeyFallback) {
139+
this.handler.setChannelKeyFallback(channelKeyFallback);
124140
return _this();
125141
}
126142

@@ -205,6 +221,20 @@ public RouterSpec<K, R> subFlowMapping(K key, IntegrationFlow subFlow) {
205221
return _this();
206222
}
207223

224+
/**
225+
* Make a default output mapping of the router to the parent flow.
226+
* Use the next, after router, parent flow {@link MessageChannel} as a
227+
* {@link AbstractMessageRouter#setDefaultOutputChannel(MessageChannel)} of this router.
228+
* This option also disables {@link AbstractMappingMessageRouter#setChannelKeyFallback(boolean)},
229+
* if not called explicitly afterwards, to skip an attempt to resolve the channel name.
230+
* @return the router spec.
231+
* @since 6.0
232+
*/
233+
public RouterSpec<K, R> defaultOutputToParentFlow() {
234+
return super.defaultOutputToParentFlow()
235+
.channelKeyFallback(false);
236+
}
237+
208238
@Override
209239
public Map<Object, String> getComponentsToRegister() {
210240
// The 'mappingProvider' must be added to the 'componentsToRegister' in the end to

spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java

+82-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -62,7 +62,7 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
6262
@SuppressWarnings("serial")
6363
private final Map<String, MessageChannel> dynamicChannels =
6464
Collections.synchronizedMap(
65-
new LinkedHashMap<String, MessageChannel>(DEFAULT_DYNAMIC_CHANNEL_LIMIT, 0.75f, true) {
65+
new LinkedHashMap<>(DEFAULT_DYNAMIC_CHANNEL_LIMIT, 0.75f, true) {
6666

6767
@Override
6868
protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) {
@@ -79,6 +79,10 @@ protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) {
7979

8080
private boolean channelKeyFallback = true;
8181

82+
private boolean defaultOutputChannelSet;
83+
84+
private boolean channelKeyFallbackSetExplicitly;
85+
8286
private volatile Map<String, String> channelMappings = new LinkedHashMap<>();
8387

8488

@@ -124,15 +128,72 @@ public void setResolutionRequired(boolean resolutionRequired) {
124128
/**
125129
* When true (default), if a resolved channel key does not exist in the channel map,
126130
* the key itself is used as the channel name, which we will attempt to resolve to a
127-
* channel. Set to false to disable this feature. This could be useful to prevent
131+
* channel. Set to {@code false} to disable this feature. This could be useful to prevent
128132
* malicious actors from generating a message that could cause the message to be
129133
* routed to an unexpected channel, such as one upstream of the router, which would
130134
* cause a stack overflow.
131-
* @param channelKeyFallback false to disable the fall back.
135+
* @param channelKeyFallback false to disable the fallback.
132136
* @since 5.2
133137
*/
134138
public void setChannelKeyFallback(boolean channelKeyFallback) {
135139
this.channelKeyFallback = channelKeyFallback;
140+
this.channelKeyFallbackSetExplicitly = true;
141+
}
142+
143+
/**
144+
* Set the default channel where Messages should be sent if channel resolution
145+
* fails to return any channels.
146+
* It also sets {@link #channelKeyFallback} to {@code false} to avoid
147+
* an attempt to resolve a channel from its key, but instead send the message
148+
* directly to this channel.
149+
* If {@link #channelKeyFallback} is set explicitly to {@code true},
150+
* the logic depends on the {@link #resolutionRequired} option
151+
* ({@code true} by default), and therefore a fallback to
152+
* this default output channel may never happen.
153+
* The configuration where a default output channel is present and
154+
* {@link #setResolutionRequired resolutionRequired} and
155+
* {@link #setChannelKeyFallback channelKeyFallback} are set to {@code true}
156+
* is rejected since it leads to ambiguity.
157+
* @param defaultOutputChannel The default output channel.
158+
* @since 6.0
159+
* @see #setChannelKeyFallback(boolean)
160+
* @see #setResolutionRequired(boolean)
161+
*/
162+
@Override
163+
public void setDefaultOutputChannel(MessageChannel defaultOutputChannel) {
164+
super.setDefaultOutputChannel(defaultOutputChannel);
165+
if (!this.channelKeyFallbackSetExplicitly) {
166+
this.channelKeyFallback = false;
167+
}
168+
this.defaultOutputChannelSet = true;
169+
}
170+
171+
/**
172+
* Set the default channel where Messages should be sent if channel resolution
173+
* fails to return any channels.
174+
* It also sets {@link #channelKeyFallback} to {@code false} to avoid
175+
* an attempt to resolve a channel from its key, but instead send the message
176+
* directly to this channel.
177+
* If {@link #channelKeyFallback} is set explicitly to {@code true},
178+
* the logic depends on the {@link #resolutionRequired} option
179+
* ({@code true} by default), and therefore a fallback to
180+
* this default output channel may never happen.
181+
* The configuration where a default output channel is present and
182+
* {@link #setResolutionRequired resolutionRequired} and
183+
* {@link #setChannelKeyFallback channelKeyFallback} are set to {@code true}
184+
* is rejected since it leads to ambiguity.
185+
* @param defaultOutputChannelName the name of the channel bean for default output.
186+
* @since 6.0
187+
* @see #setChannelKeyFallback(boolean)
188+
* @see #setResolutionRequired(boolean)
189+
*/
190+
@Override
191+
public void setDefaultOutputChannelName(String defaultOutputChannelName) {
192+
super.setDefaultOutputChannelName(defaultOutputChannelName);
193+
if (!this.channelKeyFallbackSetExplicitly) {
194+
this.channelKeyFallback = false;
195+
}
196+
this.defaultOutputChannelSet = true;
136197
}
137198

138199
/**
@@ -189,15 +250,14 @@ public Collection<String> getDynamicChannelNames() {
189250
return Collections.unmodifiableSet(this.dynamicChannels.keySet());
190251
}
191252

192-
/**
193-
* Subclasses must implement this method to return the channel keys.
194-
* A "key" might be present in this router's "channelMappings", or it
195-
* could be the channel's name or even the Message Channel instance itself.
196-
* @param message The message.
197-
* @return The channel keys.
198-
*/
199-
protected abstract List<Object> getChannelKeys(Message<?> message);
200-
253+
@Override
254+
protected void onInit() {
255+
super.onInit();
256+
Assert.state(!this.channelKeyFallback || !this.resolutionRequired || !this.defaultOutputChannelSet,
257+
"The 'defaultOutputChannel' cannot be reached " +
258+
"when both 'channelKeyFallback' & 'resolutionRequired' are set to true. " +
259+
"See their javadocs for more information.");
260+
}
201261

202262
@Override
203263
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
@@ -207,6 +267,15 @@ protected Collection<MessageChannel> determineTargetChannels(Message<?> message)
207267
return channels;
208268
}
209269

270+
/**
271+
* Subclasses must implement this method to return the channel keys.
272+
* A "key" might be present in this router's "channelMappings", or it
273+
* could be the channel's name or even the Message Channel instance itself.
274+
* @param message The message.
275+
* @return The channel keys.
276+
*/
277+
protected abstract List<Object> getChannelKeys(Message<?> message);
278+
210279
/**
211280
* Convenience method allowing conversion of a list
212281
* of mappings in a control-bus message.

spring-integration-core/src/main/kotlin/org/springframework/integration/dsl/KotlinRouterSpec.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -47,8 +47,13 @@ class KotlinRouterSpec<K, R : AbstractMappingMessageRouter>(override val delegat
4747
this.delegate.suffix(suffix)
4848
}
4949

50+
@Deprecated(message = "Since 6.0", replaceWith = ReplaceWith("channelKeyFallback(false)"))
5051
fun noChannelKeyFallback() {
51-
this.delegate.noChannelKeyFallback()
52+
channelKeyFallback(false)
53+
}
54+
55+
fun channelKeyFallback(channelKeyFallback: Boolean) {
56+
this.delegate.channelKeyFallback(channelKeyFallback)
5257
}
5358

5459
fun channelMapping(key: K, channelName: String) {

spring-integration-core/src/test/java/org/springframework/integration/dsl/routers/NoFallbackAllowedTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
/**
3535
* @author Gary Russell
36+
* @author Artem Bilan
37+
*
3638
* @since 5.2
3739
*
3840
*/
@@ -53,9 +55,7 @@ public static class Config {
5355

5456
@Bean
5557
public IntegrationFlow flow() {
56-
return f -> f.route("headers.whereTo", r -> r
57-
.noChannelKeyFallback()
58-
.defaultOutputChannel(queue()));
58+
return f -> f.route("headers.whereTo", r -> r.defaultOutputChannel(queue()));
5959
}
6060

6161
@Bean

spring-integration-core/src/test/java/org/springframework/integration/graph/IntegrationGraphServerTests.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ void test() throws Exception {
135135

136136
List<Map<?, ?>> links = (List<Map<?, ?>>) map.get("links");
137137
assertThat(links).isNotNull();
138-
assertThat(links.size()).isEqualTo(34);
138+
assertThat(links.size()).isEqualTo(32);
139139

140140
jsonArray =
141141
JsonPathUtils.evaluate(baos.toByteArray(), "$..nodes[?(@.name == 'expressionRouter.router')]");
@@ -169,7 +169,7 @@ void test() throws Exception {
169169
assertThat(nodes.size()).isEqualTo(34);
170170
links = (List<Map<?, ?>>) map.get("links");
171171
assertThat(links).isNotNull();
172-
assertThat(links.size()).isEqualTo(37);
172+
assertThat(links.size()).isEqualTo(35);
173173

174174
jsonArray = JsonPathUtils.evaluate(baos.toByteArray(), "$..nodes[?(@.name == 'router.router')]");
175175
routerJson = jsonArray.toJSONString();
@@ -347,7 +347,7 @@ public HeaderValueRouter router() {
347347
HeaderValueRouter router = new HeaderValueRouter("foo");
348348
router.setChannelMapping("bar", "barChannel");
349349
router.setChannelMapping("baz", "bazChannel");
350-
router.setDefaultOutputChannel(discards());
350+
router.setChannelKeyFallback(true);
351351
return router;
352352
}
353353

@@ -365,7 +365,7 @@ public RecipientListRouter rlRouter() {
365365
public ExpressionEvaluatingRouter expressionRouter() {
366366
ExpressionEvaluatingRouter router = new ExpressionEvaluatingRouter(
367367
new SpelExpressionParser().parseExpression("headers['foo']"));
368-
router.setDefaultOutputChannel(discards());
368+
router.setChannelKeyFallback(true);
369369
return router;
370370
}
371371

@@ -404,7 +404,7 @@ public MessageChannel fizChannel() {
404404
@Bean
405405
@InboundChannelAdapter(channel = "fizChannel", autoStartup = "false")
406406
public MessageSource<String> testSource() {
407-
return new AbstractMessageSource<String>() {
407+
return new AbstractMessageSource<>() {
408408

409409
@Override
410410
public String getComponentType() {

spring-integration-jmx/src/test/java/org/springframework/integration/jmx/config/DynamicRouterTests-context.xml

+1-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
object-name="org.springframework.integration.jmx.config:type=SimpleDynamicRouter,name=dynamicRouter"
2020
operation-name="addChannelMapping" />
2121

22-
<int:router input-channel="routingChannel" ref="dynamicRouter" method="route"
23-
default-output-channel="errorChannel"/>
22+
<int:router input-channel="routingChannel" ref="dynamicRouter" method="route"/>
2423

2524
<bean id="dynamicRouter"
2625
class="org.springframework.integration.jmx.config.SimpleDynamicRouter">

src/reference/asciidoc/router.adoc

+4-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,10 @@ If set, this attribute provides a reference to the channel where messages should
349349
If no default output channel is provided, the router throws an exception.
350350
If you would like to silently drop those messages instead, set the default output channel attribute value to `nullChannel`.
351351
+
352-
NOTE: A message is sent only to the `default-output-channel` if `resolution-required` is `false` and the channel is not resolved.
352+
NOTE: Starting with version 6.0, setting a default output channel also resets the `channelKeyFallback` option to `false`.
353+
So, no attempts will be made to resolve a channel from its name, but rather fallback to this default output channel - similar to a Java `switch` statement.
354+
If `channelKeyFallback` is set to `true` explicitly, the further logic depends on the `resolutionRequired` option: the message to non-resolved channel from key can reach a `defaultOutputChannel` only if `resolutionRequired` is `false`.
355+
Therefore, a configuration where `defaultOutputChannel` is provided and both `channelKeyFallback` & `resolutionRequired` are set to `true` is rejected by the `AbstractMappingMessageRouter` initialization phase.
353356

354357
`resolution-required`::
355358
This attribute specifies whether channel names must always be successfully resolved to channel instances that exist.

src/reference/asciidoc/whats-new.adoc

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ For convenience, the XML and Java DSL for Scatter-Gather, based on the `Recipien
110110

111111
See <<./scatter-gather.adoc#scatter-gather,Scatter-Gather>> for more information.
112112

113+
Another convenient behavior change has been made to the `AbstractMappingMessageRouter`.
114+
Now, setting a `defaultOutputChannel` also resets the `channelKeyFallback` property to `false`, so no attempts will be made to resolve a channel from its key, but the logic immediately falls back to sending the message to the `defaultOutputChannel`.
115+
116+
See <<./router.adoc#router-common-parameters-all,Router Options>> for more information.
117+
113118
The `AggregatingMessageHandler` now does not split a `Collection<Message<?>>` result of the `MessageGroupProcessor` (unless it is a `SimpleMessageGroupProcessor`) on the output, but emits a single message containing this whole collection as a payload.
114119

115120
See <<./aggregator.adoc#aggregator,Aggregator>> for more information.

0 commit comments

Comments
 (0)