INT-3816: Superclass Mapping for EMExcTypeRouter

JIRA: https://jira.spring.io/browse/INT-3816

Add `cause` `supperclass` mapping ability to the `ErrorMessageExceptionTypeRouter`

Address PR comments:

Introduce `classNameMappings` to store exception classes on the mapping population.
Override all `@ManagedOperation` s to modify `classNameMappings` as well.

Address PR comments:

* Remove redundant local variable
* Fix concurrency access around `channelMapping`
* Add `ClassNotFoundException` test for the `ErrorMessageExceptionTypeRouter`
* Add `What's New` note about the `ClassNotFoundException` during init

Get rid of `synchronized`, use atomic changes

Additional polishing to avoid `ConcurrentModificationException `

Polishing
This commit is contained in:
Artem Bilan
2016-01-14 16:33:21 -05:00
committed by Gary Russell
parent 1760572c8b
commit 2d33495437
6 changed files with 186 additions and 58 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,15 +19,13 @@ package org.springframework.integration.router;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.integration.support.management.MappingMessageRouterManagement;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
@@ -52,7 +50,7 @@ import org.springframework.util.StringUtils;
*/
public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter implements MappingMessageRouterManagement {
private volatile Map<String, String> channelMappings = new ConcurrentHashMap<String, String>();
protected volatile Map<String, String> channelMappings = new ConcurrentHashMap<String, String>();
private volatile String prefix;
@@ -70,9 +68,8 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
@ManagedAttribute
public void setChannelMappings(Map<String, String> channelMappings) {
Assert.notNull(channelMappings, "'channelMappings' must not be null");
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>();
newChannelMappings.putAll(channelMappings);
this.doSetChannelMappings(newChannelMappings);
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(channelMappings);
doSetChannelMappings(newChannelMappings);
}
/**
@@ -108,7 +105,7 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
@Override
@ManagedAttribute
public Map<String, String> getChannelMappings() {
return Collections.unmodifiableMap(this.channelMappings);
return new HashMap<String, String>(this.channelMappings);
}
/**
@@ -119,7 +116,9 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
@Override
@ManagedOperation
public void setChannelMapping(String key, String channelName) {
this.channelMappings.put(key, channelName);
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings);
newChannelMappings.put(key, channelName);
this.channelMappings = newChannelMappings;
}
/**
@@ -129,7 +128,9 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
@Override
@ManagedOperation
public void removeChannelMapping(String key) {
this.channelMappings.remove(key);
Map<String, String> newChannelMappings = new ConcurrentHashMap<String, String>(this.channelMappings);
newChannelMappings.remove(key);
this.channelMappings = newChannelMappings;
}
/**
@@ -176,8 +177,7 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter
Map<String, String> oldChannelMappings = this.channelMappings;
this.channelMappings = newChannelMappings;
if (logger.isDebugEnabled()) {
logger.debug("Channel mappings:" + oldChannelMappings
+ " replaced with:" + newChannelMappings);
logger.debug("Channel mappings: " + oldChannelMappings + " replaced with: " + newChannelMappings);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,20 +18,93 @@ package org.springframework.integration.router;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.util.ClassUtils;
/**
* A Message Router that resolves the target {@link MessageChannel} for
* messages whose payload is an Exception. The channel resolution is based upon
* the most specific cause of the error for which a channel-mapping exists.
*
* <p>
* The channel-mapping can be specified for the super classes to avoid mapping duplication
* for the particular exception implementation.
*
* @author Mark Fisher
* @author Oleg Zhurakousky
*/
* @author Artem Bilan
*/
public class ErrorMessageExceptionTypeRouter extends AbstractMappingMessageRouter {
private volatile Map<String, Class<?>> classNameMappings = new ConcurrentHashMap<String, Class<?>>();
private volatile boolean initialized;
@Override
@ManagedAttribute
public void setChannelMappings(Map<String, String> channelMappings) {
super.setChannelMappings(channelMappings);
if (this.initialized) {
populateClassNameMapping(channelMappings.keySet());
}
}
private void populateClassNameMapping(Set<String> classNames) {
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<String, Class<?>>();
for (String className : classNames) {
newClassNameMappings.put(className, resolveClassFromName(className));
}
this.classNameMappings = newClassNameMappings;
}
private Class<?> resolveClassFromName(String className) {
try {
return ClassUtils.forName(className, getApplicationContext().getClassLoader());
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Cannot load class for channel mapping.", e);
}
}
@Override
@ManagedOperation
public void setChannelMapping(String key, String channelName) {
super.setChannelMapping(key, channelName);
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<String, Class<?>>(this.classNameMappings);
newClassNameMappings.put(key, resolveClassFromName(key));
this.classNameMappings = newClassNameMappings;
}
@Override
@ManagedOperation
public void removeChannelMapping(String key) {
super.removeChannelMapping(key);
Map<String, Class<?>> newClassNameMappings = new ConcurrentHashMap<String, Class<?>>(this.classNameMappings);
newClassNameMappings.remove(key);
this.classNameMappings = newClassNameMappings;
}
@Override
@ManagedOperation
public void replaceChannelMappings(Properties channelMappings) {
super.replaceChannelMappings(channelMappings);
populateClassNameMapping(this.channelMappings.keySet());
}
@Override
protected void onInit() throws Exception {
super.onInit();
populateClassNameMapping(this.channelMappings.keySet());
this.initialized = true;
}
@Override
protected List<Object> getChannelKeys(Message<?> message) {
String mostSpecificCause = null;
@@ -39,14 +112,17 @@ public class ErrorMessageExceptionTypeRouter extends AbstractMappingMessageRoute
if (payload instanceof Throwable) {
Throwable cause = (Throwable) payload;
while (cause != null) {
String causeName = cause.getClass().getName();
if (this.getChannelMappings().keySet().contains(causeName)) {
mostSpecificCause = causeName;
for (Map.Entry<String, Class<?>> entry : this.classNameMappings.entrySet()) {
String channelKey = entry.getKey();
Class<?> exceptionClass = entry.getValue();
if (exceptionClass.isInstance(cause)) {
mostSpecificCause = channelKey;
}
}
cause = cause.getCause();
}
}
return Collections.singletonList((Object) mostSpecificCause);
return Collections.<Object>singletonList(mostSpecificCause);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,7 +47,7 @@ public class PayloadTypeRouter extends AbstractMappingMessageRouter {
*/
@Override
protected List<Object> getChannelKeys(Message<?> message) {
if (CollectionUtils.isEmpty(this.getChannelMappings())) {
if (CollectionUtils.isEmpty(this.channelMappings)) {
return null;
}
Class<?> type = message.getPayload().getClass();
@@ -63,7 +63,7 @@ public class PayloadTypeRouter extends AbstractMappingMessageRouter {
private String findClosestMatch(Class<?> type, boolean isArray) {
int minTypeDiffWeight = Integer.MAX_VALUE;
List<String> matches = new ArrayList<String>();
for (String candidate : this.getChannelMappings().keySet()) {
for (String candidate : this.channelMappings.keySet()) {
if (isArray) {
if (!candidate.endsWith(ARRAY_SUFFIX)) {
continue;
@@ -88,7 +88,8 @@ public class PayloadTypeRouter extends AbstractMappingMessageRouter {
}
if (matches.size() > 1) { // ambiguity
throw new IllegalStateException(
"Unresolvable ambiguity while attempting to find closest match for [" + type.getName() + "]. Found: " + matches);
"Unresolvable ambiguity while attempting to find closest match for [" + type.getName() + "]. " +
"Found: " + matches);
}
if (CollectionUtils.isEmpty(matches)) { // no match
return null;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,19 +23,18 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.integration.MessageRejectedException;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
/**
* @author Mark Fisher
@@ -74,16 +73,15 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
exceptionTypeChannelMap.put(RuntimeException.class.getName(), "runtimeExceptionChannel");
exceptionTypeChannelMap.put(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(illegalArgumentChannel.receive(1000));
assertNull(defaultChannel.receive(0));
assertNull(runtimeExceptionChannel.receive(0));
@@ -98,14 +96,14 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(RuntimeException.class.getName(), "runtimeExceptionChannel");
exceptionTypeChannelMap.put(MessageHandlingException.class.getName(), "runtimeExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
router.setChannelMapping(MessageHandlingException.class.getName(), "runtimeExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(runtimeExceptionChannel.receive(1000));
assertNull(illegalArgumentChannel.receive(0));
assertNull(defaultChannel.receive(0));
@@ -120,12 +118,13 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(messageHandlingExceptionChannel.receive(1000));
assertNull(runtimeExceptionChannel.receive(0));
assertNull(illegalArgumentChannel.receive(0));
@@ -140,8 +139,11 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(defaultChannel.receive(1000));
assertNull(runtimeExceptionChannel.receive(0));
assertNull(illegalArgumentChannel.receive(0));
@@ -156,12 +158,12 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(MessageDeliveryException.class.getName(), "messageDeliveryExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(MessageDeliveryException.class.getName(), "messageDeliveryExceptionChannel");
router.setResolutionRequired(true);
router.setBeanName("fooRouter");
try {
router.handleMessage(message);
fail("MessageDeliveryException expected");
@@ -180,14 +182,15 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
Message<?> message = new GenericMessage<Exception>(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
exceptionTypeChannelMap.put(RuntimeException.class.getName(), "runtimeExceptionChannel");
exceptionTypeChannelMap.put(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
router.setChannelMapping(RuntimeException.class.getName(), "runtimeExceptionChannel");
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(illegalArgumentChannel.receive(1000));
assertNull(defaultChannel.receive(0));
assertNull(runtimeExceptionChannel.receive(0));
@@ -202,17 +205,50 @@ public class ErrorMessageExceptionTypeRouterTests {
MessageHandlingException error = new MessageHandlingException(failedMessage, "failed", middleCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
Map<String, String> exceptionTypeChannelMap = new HashMap<String, String>();
exceptionTypeChannelMap.put(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
exceptionTypeChannelMap.put(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setChannelMappings(exceptionTypeChannelMap);
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(IllegalArgumentException.class.getName(), "illegalArgumentChannel");
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(illegalArgumentChannel.receive(1000));
assertNull(defaultChannel.receive(0));
assertNull(runtimeExceptionChannel.receive(0));
assertNull(messageHandlingExceptionChannel.receive(0));
}
@Test
public void testHierarchicalMapping() {
IllegalArgumentException rootCause = new IllegalArgumentException("bad argument");
MessageHandlingException error = new MessageRejectedException(new GenericMessage<Object>("foo"), "failed", rootCause);
ErrorMessage message = new ErrorMessage(error);
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
router.setChannelMapping(MessageHandlingException.class.getName(), "messageHandlingExceptionChannel");
router.setDefaultOutputChannel(defaultChannel);
router.handleMessage(message);
assertNotNull(messageHandlingExceptionChannel.receive(1000));
assertNull(defaultChannel.receive(0));
}
@Test
public void testInvalidMapping() {
ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter();
router.setBeanFactory(beanFactory);
router.setApplicationContext(TestUtils.createTestApplicationContext());
try {
router.setChannelMapping("foo", "fooChannel");
fail("IllegalStateException expected");
}
catch (Exception e) {
assertThat(e, instanceOf(IllegalStateException.class));
assertThat(e.getCause(), instanceOf(ClassNotFoundException.class));
}
}
}

View File

@@ -959,7 +959,13 @@ As such, please read chapter _<<xml-xpath-routing>>_
Spring Integration also provides a special type-based router called `ErrorMessageExceptionTypeRouter` for routing Error Messages (Messages whose `payload` is a `Throwable` instance).
`ErrorMessageExceptionTypeRouter` is very similar to the `PayloadTypeRouter`.
In fact they are almost identical.
The only difference is that while `PayloadTypeRouter` navigates the instance hierarchy of a payload instance (e.g., `payload.getClass().getSuperclass()`) to find the most specific type/channel mappings, the `ErrorMessageExceptionTypeRouter` navigates the hierarchy of 'exception causes' (e.g., `payload.getCause()`) to find the most specific `Throwable` type/channel mappings.
The only difference is that while `PayloadTypeRouter` navigates the instance hierarchy of a payload instance (e.g., `payload.getClass().getSuperclass()`) to find the most specific type/channel mappings,
the `ErrorMessageExceptionTypeRouter` navigates the hierarchy of 'exception causes' (e.g., `payload.getCause()`)
to find the most specific `Throwable` type/channel mappings and uses `mappingClass.isInstance(cause)` to match the
`cause` to the class or any super class.
NOTE: Since _version 4.3_ the `ErrorMessageExceptionTypeRouter` loads all mapping classes during the initialization
phase to fail-fast for a `ClassNotFoundException`.
Below is a sample configuration for `ErrorMessageExceptionTypeRouter`.

View File

@@ -97,3 +97,12 @@ See <<http-inbound>> for more information.
A new factory bean is provided to simplify the configuration of Jsch proxies for SFTP.
See <<sftp-proxy-factory-bean>> for more information.
==== Routers Changes
The `ErrorMessageExceptionTypeRouter` supports now the `Exception` superclass mappings to avoid duplication
for the same channel in case of several inheritors.
For this purpose the `ErrorMessageExceptionTypeRouter` loads mapping classes during initialization to fail-fast
for a `ClassNotFoundException`.
See <<router>> for more information.