Skip to content

Commit 23a73fa

Browse files
artembilangaryrussell
authored andcommitted
GH-2873: Preserve mapping order in the router (#2877)
* GH-2873: Preserve mapping order in the router Fixes #2873 Sometime it is important to map to most specific exception instead of its super class. * Use `LinkedHashMap` for mapping keys in the `ErrorMessageExceptionTypeRouter`, as well as in its `AbstractMappingMessageRouter` superclass. Since we don't do that internal map modification, there is no reason to worry about concurrent access: we just replace an internal instance atomically with a new `LinkedHashMap` every time we modify a mapping for router **Cherry-pick to 5.1.x** * * Fix `RouterSpec.RouterMappingProvider` to `LinkedHashMap` as well * * Fix `RouterTests` for proper mapping order * Polishing for `AbstractMappingMessageRouter` hierarchy, so we don't use `protected channelMappings` field access any more
1 parent 3fc2786 commit 23a73fa

File tree

7 files changed

+51
-55
lines changed

7 files changed

+51
-55
lines changed

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

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

1717
package org.springframework.integration.dsl;
1818

19-
import java.util.HashMap;
19+
import java.util.LinkedHashMap;
2020
import java.util.Map;
2121

2222
import org.springframework.core.convert.ConversionService;
@@ -188,7 +188,7 @@ private static class RouterMappingProvider extends IntegrationObjectSupport {
188188

189189
private final MappingMessageRouterManagement router;
190190

191-
private final Map<Object, NamedComponent> mapping = new HashMap<>();
191+
private final Map<Object, NamedComponent> mapping = new LinkedHashMap<>();
192192

193193
RouterMappingProvider(MappingMessageRouterManagement router) {
194194
this.router = router;

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

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,12 @@
2020
import java.util.Arrays;
2121
import java.util.Collection;
2222
import java.util.Collections;
23-
import java.util.HashMap;
2423
import java.util.LinkedHashMap;
2524
import java.util.List;
2625
import java.util.Map;
2726
import java.util.Map.Entry;
2827
import java.util.Properties;
2928
import java.util.Set;
30-
import java.util.concurrent.ConcurrentHashMap;
3129

3230
import org.springframework.integration.support.management.MappingMessageRouterManagement;
3331
import org.springframework.jmx.export.annotation.ManagedAttribute;
@@ -48,32 +46,35 @@
4846
* @author Gunnar Hillert
4947
* @author Gary Russell
5048
* @author Artem Bilan
49+
*
5150
* @since 2.1
5251
*/
53-
public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter implements MappingMessageRouterManagement {
52+
public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
53+
implements MappingMessageRouterManagement {
5454

5555
private static final int DEFAULT_DYNAMIC_CHANNEL_LIMIT = 100;
5656

5757
private int dynamicChannelLimit = DEFAULT_DYNAMIC_CHANNEL_LIMIT;
5858

5959
@SuppressWarnings("serial")
60-
private final Map<String, MessageChannel> dynamicChannels = Collections.<String, MessageChannel>synchronizedMap(
61-
new LinkedHashMap<String, MessageChannel>(DEFAULT_DYNAMIC_CHANNEL_LIMIT, 0.75f, true) {
60+
private final Map<String, MessageChannel> dynamicChannels =
61+
Collections.synchronizedMap(
62+
new LinkedHashMap<String, MessageChannel>(DEFAULT_DYNAMIC_CHANNEL_LIMIT, 0.75f, true) {
6263

63-
@Override
64-
protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) {
65-
return this.size() > AbstractMappingMessageRouter.this.dynamicChannelLimit;
66-
}
64+
@Override
65+
protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) {
66+
return this.size() > AbstractMappingMessageRouter.this.dynamicChannelLimit;
67+
}
6768

68-
});
69+
});
6970

70-
protected volatile Map<String, String> channelMappings = new ConcurrentHashMap<String, String>();
71+
private String prefix;
7172

72-
private volatile String prefix;
73+
private String suffix;
7374

74-
private volatile String suffix;
75+
private boolean resolutionRequired = true;
7576

76-
private volatile boolean resolutionRequired = true;
77+
private volatile Map<String, String> channelMappings = new LinkedHashMap<>();
7778

7879

7980
/**
@@ -86,7 +87,7 @@ protected boolean removeEldestEntry(Entry<String, MessageChannel> eldest) {
8687
@ManagedAttribute
8788
public void setChannelMappings(Map<String, String> channelMappings) {
8889
Assert.notNull(channelMappings, "'channelMappings' must not be null");
89-
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(channelMappings);
90+
Map<String, String> newChannelMappings = new LinkedHashMap<>(channelMappings);
9091
doSetChannelMappings(newChannelMappings);
9192
}
9293

@@ -135,7 +136,7 @@ public void setDynamicChannelLimit(int dynamicChannelLimit) {
135136
@Override
136137
@ManagedAttribute
137138
public Map<String, String> getChannelMappings() {
138-
return new HashMap<String, String>(this.channelMappings);
139+
return new LinkedHashMap<>(this.channelMappings);
139140
}
140141

141142
/**
@@ -146,7 +147,7 @@ public Map<String, String> getChannelMappings() {
146147
@Override
147148
@ManagedOperation
148149
public void setChannelMapping(String key, String channelName) {
149-
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings);
150+
Map<String, String> newChannelMappings = new LinkedHashMap<>(this.channelMappings);
150151
newChannelMappings.put(key, channelName);
151152
this.channelMappings = newChannelMappings;
152153
}
@@ -158,7 +159,7 @@ public void setChannelMapping(String key, String channelName) {
158159
@Override
159160
@ManagedOperation
160161
public void removeChannelMapping(String key) {
161-
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings);
162+
Map<String, String> newChannelMappings = new LinkedHashMap<>(this.channelMappings);
162163
newChannelMappings.remove(key);
163164
this.channelMappings = newChannelMappings;
164165
}
@@ -181,8 +182,8 @@ public Collection<String> getDynamicChannelNames() {
181182

182183
@Override
183184
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
184-
Collection<MessageChannel> channels = new ArrayList<MessageChannel>();
185-
Collection<Object> channelKeys = this.getChannelKeys(message);
185+
Collection<MessageChannel> channels = new ArrayList<>();
186+
Collection<Object> channelKeys = getChannelKeys(message);
186187
addToCollection(channels, channelKeys, message);
187188
return channels;
188189
}
@@ -201,12 +202,12 @@ protected Collection<MessageChannel> determineTargetChannels(Message<?> message)
201202
@ManagedOperation
202203
public void replaceChannelMappings(Properties channelMappings) {
203204
Assert.notNull(channelMappings, "'channelMappings' must not be null");
204-
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>();
205+
Map<String, String> newChannelMappings = new LinkedHashMap<>();
205206
Set<String> keys = channelMappings.stringPropertyNames();
206207
for (String key : keys) {
207208
newChannelMappings.put(key.trim(), channelMappings.getProperty(key).trim());
208209
}
209-
this.doSetChannelMappings(newChannelMappings);
210+
doSetChannelMappings(newChannelMappings);
210211
}
211212

212213
private void doSetChannelMappings(Map<String, String> newChannelMappings) {
@@ -227,9 +228,6 @@ private MessageChannel resolveChannelForName(String channelName, Message<?> mess
227228
throw new MessagingException(message, "failed to resolve channel name '" + channelName + "'", e);
228229
}
229230
}
230-
if (channel == null && this.resolutionRequired) {
231-
throw new MessagingException(message, "failed to resolve channel name '" + channelName + "'");
232-
}
233231
return channel;
234232
}
235233

@@ -258,7 +256,7 @@ private void addChannelFromString(Collection<MessageChannel> channels, String ch
258256
MessageChannel channel = resolveChannelForName(channelName, message);
259257
if (channel != null) {
260258
channels.add(channel);
261-
if (!mapped && !(this.dynamicChannels.get(channelName) != null)) {
259+
if (!mapped && this.dynamicChannels.get(channelName) == null) {
262260
this.dynamicChannels.put(channelName, channel);
263261
}
264262
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
package org.springframework.integration.router;
1818

1919
import java.util.Collections;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
2122
import java.util.Map;
2223
import java.util.Properties;
2324
import java.util.Set;
24-
import java.util.concurrent.ConcurrentHashMap;
2525

2626
import org.springframework.jmx.export.annotation.ManagedAttribute;
2727
import org.springframework.jmx.export.annotation.ManagedOperation;
@@ -46,7 +46,7 @@
4646
*/
4747
public class ErrorMessageExceptionTypeRouter extends AbstractMappingMessageRouter {
4848

49-
private volatile Map<String, Class<?>> classNameMappings = new ConcurrentHashMap<>();
49+
private volatile Map<String, Class<?>> classNameMappings = new LinkedHashMap<>();
5050

5151
private volatile boolean initialized;
5252

@@ -60,7 +60,7 @@ public void setChannelMappings(Map<String, String> channelMappings) {
6060
}
6161

6262
private void populateClassNameMapping(Set<String> classNames) {
63-
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<>();
63+
Map<String, Class<?>> newClassNameMappings = new LinkedHashMap<>();
6464
for (String className : classNames) {
6565
newClassNameMappings.put(className, resolveClassFromName(className));
6666
}
@@ -82,7 +82,7 @@ private Class<?> resolveClassFromName(String className) {
8282
public void setChannelMapping(String key, String channelName) {
8383
super.setChannelMapping(key, channelName);
8484
if (this.initialized) {
85-
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<>(this.classNameMappings);
85+
Map<String, Class<?>> newClassNameMappings = new LinkedHashMap<>(this.classNameMappings);
8686
newClassNameMappings.put(key, resolveClassFromName(key));
8787
this.classNameMappings = newClassNameMappings;
8888
}
@@ -92,7 +92,7 @@ public void setChannelMapping(String key, String channelName) {
9292
@ManagedOperation
9393
public void removeChannelMapping(String key) {
9494
super.removeChannelMapping(key);
95-
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<>(this.classNameMappings);
95+
Map<String, Class<?>> newClassNameMappings = new LinkedHashMap<>(this.classNameMappings);
9696
newClassNameMappings.remove(key);
9797
this.classNameMappings = newClassNameMappings;
9898
}
@@ -101,13 +101,13 @@ public void removeChannelMapping(String key) {
101101
@ManagedOperation
102102
public void replaceChannelMappings(Properties channelMappings) {
103103
super.replaceChannelMappings(channelMappings);
104-
populateClassNameMapping(this.channelMappings.keySet());
104+
populateClassNameMapping(getChannelMappings().keySet());
105105
}
106106

107107
@Override
108108
protected void onInit() {
109109
super.onInit();
110-
populateClassNameMapping(this.channelMappings.keySet());
110+
populateClassNameMapping(getChannelMappings().keySet());
111111
this.initialized = true;
112112
}
113113

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@
1616

1717
package org.springframework.integration.router;
1818

19-
import java.util.ArrayList;
2019
import java.util.Collections;
20+
import java.util.LinkedList;
2121
import java.util.List;
2222

2323
import org.springframework.messaging.Message;
2424
import org.springframework.util.CollectionUtils;
2525

2626
/**
2727
* A Message Router that resolves the {@link org.springframework.messaging.MessageChannel}
28-
* based on the
29-
* {@link Message Message's} payload type.
28+
* based on the {@link Message Message's} payload type.
3029
*
3130
* @author Mark Fisher
3231
* @author Oleg Zhurakousky
3332
* @author Gary Russell
33+
* @author Artem Bilan
3434
*/
3535
public class PayloadTypeRouter extends AbstractMappingMessageRouter {
3636

@@ -47,23 +47,22 @@ public class PayloadTypeRouter extends AbstractMappingMessageRouter {
4747
*/
4848
@Override
4949
protected List<Object> getChannelKeys(Message<?> message) {
50-
if (CollectionUtils.isEmpty(this.channelMappings)) {
50+
if (CollectionUtils.isEmpty(getChannelMappings())) {
5151
return null;
5252
}
5353
Class<?> type = message.getPayload().getClass();
5454
boolean isArray = type.isArray();
5555
if (isArray) {
5656
type = type.getComponentType();
5757
}
58-
String closestMatch = this.findClosestMatch(type, isArray);
59-
return (closestMatch != null) ? Collections.<Object>singletonList(closestMatch) : null;
58+
String closestMatch = findClosestMatch(type, isArray);
59+
return (closestMatch != null) ? Collections.singletonList(closestMatch) : null;
6060
}
6161

62-
6362
private String findClosestMatch(Class<?> type, boolean isArray) {
6463
int minTypeDiffWeight = Integer.MAX_VALUE;
65-
List<String> matches = new ArrayList<String>();
66-
for (String candidate : this.channelMappings.keySet()) {
64+
List<String> matches = new LinkedList<>();
65+
for (String candidate : getChannelMappings().keySet()) {
6766
if (isArray) {
6867
if (!candidate.endsWith(ARRAY_SUFFIX)) {
6968
continue;
@@ -118,7 +117,7 @@ private int determineTypeDifferenceWeight(String candidate, Class<?> type, int l
118117
// exhausted hierarchy and found no match
119118
return Integer.MAX_VALUE;
120119
}
121-
return this.determineTypeDifferenceWeight(candidate, type.getSuperclass(), level + 2);
120+
return determineTypeDifferenceWeight(candidate, type.getSuperclass(), level + 2);
122121
}
123122

124123
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,8 +743,8 @@ public IntegrationFlow payloadTypeRouteFlow() {
743743
public IntegrationFlow exceptionTypeRouteFlow() {
744744
return f -> f
745745
.routeByException(r -> r
746-
.channelMapping(IllegalArgumentException.class, "illegalArgumentChannel")
747746
.channelMapping(RuntimeException.class, "runtimeExceptionChannel")
747+
.channelMapping(IllegalArgumentException.class, "illegalArgumentChannel")
748748
.subFlowMapping(MessageHandlingException.class, sf ->
749749
sf.channel("messageHandlingExceptionChannel"))
750750
.defaultOutputChannel("exceptionRouterDefaultChannel"));

spring-integration-core/src/test/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouterTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ public void mostSpecificCause() {
7979
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
8080
router.setBeanFactory(this.context);
8181
router.setApplicationContext(this.context);
82-
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
8382
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
83+
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
8484
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
8585
router.setDefaultOutputChannel(this.defaultChannel);
8686
router.afterPropertiesSet();
@@ -104,7 +104,7 @@ public void fallbackToNextMostSpecificCause() {
104104
router.setBeanFactory(this.context);
105105
router.setApplicationContext(this.context);
106106
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
107-
router.setChannelMapping(MessageHandlingException.class.getName(), "runtimeExceptionChannel");
107+
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
108108
router.setDefaultOutputChannel(this.defaultChannel);
109109
router.afterPropertiesSet();
110110

@@ -192,8 +192,8 @@ public void exceptionPayloadButNotErrorMessage() {
192192
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
193193
router.setBeanFactory(this.context);
194194
router.setApplicationContext(this.context);
195-
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
196195
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
196+
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
197197
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
198198
router.setDefaultOutputChannel(this.defaultChannel);
199199
router.afterPropertiesSet();

src/reference/asciidoc/router.adoc

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -750,13 +750,12 @@ See <<xml-xpath-routing>>.
750750
Spring Integration also provides a special type-based router called `ErrorMessageExceptionTypeRouter` for routing error messages (defined as messages whose `payload` is a `Throwable` instance).
751751
`ErrorMessageExceptionTypeRouter` is similar to the `PayloadTypeRouter`.
752752
In fact, they are almost identical.
753-
The only difference is that, while `PayloadTypeRouter` navigates the instance hierarchy of a payload instance (for example, `payload.getClass().getSuperclass()`) to find the most specific type and channel mappings,
754-
the `ErrorMessageExceptionTypeRouter` navigates the hierarchy of 'exception causes' (for example, `payload.getCause()`)
755-
to find the most specific `Throwable` type or channel mappings and uses `mappingClass.isInstance(cause)` to match the
756-
`cause` to the class or any super class.
753+
The only difference is that, while `PayloadTypeRouter` navigates the instance hierarchy of a payload instance (for example, `payload.getClass().getSuperclass()`) to find the most specific type and channel mappings, the `ErrorMessageExceptionTypeRouter` navigates the hierarchy of 'exception causes' (for example, `payload.getCause()`) to find the most specific `Throwable` type or channel mappings and uses `mappingClass.isInstance(cause)` to match the `cause` to the class or any super class.
757754

758-
NOTE: Since version 4.3 the `ErrorMessageExceptionTypeRouter` loads all mapping classes during the initialization
759-
phase to fail-fast for a `ClassNotFoundException`.
755+
IMPORTANT: The channel mapping order in this case matters.
756+
So, if there is a requirement to get mapping for an `IllegalArgumentException`, but not a `RuntimeException`, the last one must be configured on router first.
757+
758+
NOTE: Since version 4.3 the `ErrorMessageExceptionTypeRouter` loads all mapping classes during the initialization phase to fail-fast for a `ClassNotFoundException`.
760759

761760
The following example shows a sample configuration for `ErrorMessageExceptionTypeRouter`:
762761

0 commit comments

Comments
 (0)