From 2d334954372818adbab18a2ae35a6e00c4e4ba8d Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Thu, 14 Jan 2016 16:33:21 -0500 Subject: [PATCH] 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 --- .../router/AbstractMappingMessageRouter.java | 26 ++--- .../ErrorMessageExceptionTypeRouter.java | 90 ++++++++++++++-- .../integration/router/PayloadTypeRouter.java | 9 +- .../ErrorMessageExceptionTypeRouterTests.java | 102 ++++++++++++------ src/reference/asciidoc/router.adoc | 8 +- src/reference/asciidoc/whats-new.adoc | 9 ++ 6 files changed, 186 insertions(+), 58 deletions(-) diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java index 5acbcb516c..bcee06323b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/AbstractMappingMessageRouter.java @@ -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 channelMappings = new ConcurrentHashMap(); + protected volatile Map channelMappings = new ConcurrentHashMap(); private volatile String prefix; @@ -70,9 +68,8 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter @ManagedAttribute public void setChannelMappings(Map channelMappings) { Assert.notNull(channelMappings, "'channelMappings' must not be null"); - Map newChannelMappings = new ConcurrentHashMap(); - newChannelMappings.putAll(channelMappings); - this.doSetChannelMappings(newChannelMappings); + Map newChannelMappings = new ConcurrentHashMap(channelMappings); + doSetChannelMappings(newChannelMappings); } /** @@ -108,7 +105,7 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter @Override @ManagedAttribute public Map getChannelMappings() { - return Collections.unmodifiableMap(this.channelMappings); + return new HashMap(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 newChannelMappings = new ConcurrentHashMap(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 newChannelMappings = new ConcurrentHashMap(this.channelMappings); + newChannelMappings.remove(key); + this.channelMappings = newChannelMappings; } /** @@ -176,8 +177,7 @@ public abstract class AbstractMappingMessageRouter extends AbstractMessageRouter Map 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); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouter.java index 3d441e0796..d2e644b89a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouter.java @@ -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. - * + *

+ * 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> classNameMappings = new ConcurrentHashMap>(); + + private volatile boolean initialized; + + @Override + @ManagedAttribute + public void setChannelMappings(Map channelMappings) { + super.setChannelMappings(channelMappings); + if (this.initialized) { + populateClassNameMapping(channelMappings.keySet()); + } + } + + private void populateClassNameMapping(Set classNames) { + Map> newClassNameMappings = new ConcurrentHashMap>(); + 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> newClassNameMappings = new ConcurrentHashMap>(this.classNameMappings); + newClassNameMappings.put(key, resolveClassFromName(key)); + this.classNameMappings = newClassNameMappings; + } + + @Override + @ManagedOperation + public void removeChannelMapping(String key) { + super.removeChannelMapping(key); + Map> newClassNameMappings = new ConcurrentHashMap>(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 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> 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.singletonList(mostSpecificCause); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/router/PayloadTypeRouter.java b/spring-integration-core/src/main/java/org/springframework/integration/router/PayloadTypeRouter.java index d0b414d265..c755a0ae8a 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/router/PayloadTypeRouter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/router/PayloadTypeRouter.java @@ -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 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 matches = new ArrayList(); - 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; diff --git a/spring-integration-core/src/test/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouterTests.java b/spring-integration-core/src/test/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouterTests.java index 59460c5a19..f0e12786df 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouterTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/router/ErrorMessageExceptionTypeRouterTests.java @@ -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 exceptionTypeChannelMap = new HashMap(); - 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 exceptionTypeChannelMap = new HashMap(); - 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 exceptionTypeChannelMap = new HashMap(); - 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 exceptionTypeChannelMap = new HashMap(); - 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(error); ErrorMessageExceptionTypeRouter router = new ErrorMessageExceptionTypeRouter(); - Map exceptionTypeChannelMap = new HashMap(); - 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 exceptionTypeChannelMap = new HashMap(); - 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("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)); + } + } + } diff --git a/src/reference/asciidoc/router.adoc b/src/reference/asciidoc/router.adoc index 80de1548c7..321825f5b8 100644 --- a/src/reference/asciidoc/router.adoc +++ b/src/reference/asciidoc/router.adoc @@ -959,7 +959,13 @@ As such, please read chapter _<>_ 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`. diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 4c4c03bc63..1a122acf56 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -97,3 +97,12 @@ See <> for more information. A new factory bean is provided to simplify the configuration of Jsch proxies for SFTP. See <> 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 <> for more information.