From 5b0ede6a7c72e80126fbf1fa78ea162b2a43ebfc Mon Sep 17 00:00:00 2001 From: markpollack Date: Wed, 16 Dec 2009 19:00:48 +0000 Subject: [PATCH] SPRNET-1300 - NMS custom schema is missing a way to set the Exception Listener when creating message listener containers SPRNET-1302 - Add IErrorHandler interface to be called when exception thrown by message processing is not of vendor specific messaging exception type. --- doc/reference/src/messaging.xml | 20 ++ .../Spring.Core/Spring.Core.2003.csproj | 15 +- .../Spring.Core/Spring.Core.2005.csproj | 1 + .../Spring.Core/Spring.Core.2008.csproj | 1 + src/Spring/Spring.Core/Util/IErrorHandler.cs | 42 +++ ...ListenerContainerObjectDefinitionParser.cs | 25 +- .../Messaging/Ems/Config/spring-ems-1.3.xsd | 22 +- .../AbstractMessageListenerContainer.cs | 29 +- ...ListenerContainerObjectDefinitionParser.cs | 26 +- .../Nms/Config/NmsNamespaceParser.cs | 2 +- .../Messaging/Nms/Config/spring-nms-1.3.xsd | 250 ++++++++++++++++++ .../AbstractMessageListenerContainer.cs | 35 ++- .../Spring.Messaging.Nms.2008.csproj | 3 + .../SimpleMessageListenerContainerTests.cs | 50 +++- .../SimpleMessageListenerContainerTests.cs | 44 ++- 15 files changed, 537 insertions(+), 28 deletions(-) create mode 100644 src/Spring/Spring.Core/Util/IErrorHandler.cs create mode 100644 src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/spring-nms-1.3.xsd diff --git a/doc/reference/src/messaging.xml b/doc/reference/src/messaging.xml index dc9c4745..0a210d0b 100644 --- a/doc/reference/src/messaging.xml +++ b/doc/reference/src/messaging.xml @@ -1428,6 +1428,26 @@ namespace MyApp initialization. Default is true, optionally set to false. + + + error-handler + + A reference to a IErrorHandler that will handle any + uncaught exceptions other than those of the type NMSException + (in the case of ActiveMQ or EMSException int he case of TIBCO + EMS) that may occur during the execution of the message + listener. By default no ErrorHandler is registered and that + error-level logging is the default behavior. + + + + exception-listener + + A reference to an + Spring.Messaging.Nms.Core.IExceptionListener or + TIBCO.EMS.IExceptionListener as appropriate. Is invokved in case + of a NMSException or EMSException. + diff --git a/src/Spring/Spring.Core/Spring.Core.2003.csproj b/src/Spring/Spring.Core/Spring.Core.2003.csproj index 7b5eb9b5..03ebab20 100644 --- a/src/Spring/Spring.Core/Spring.Core.2003.csproj +++ b/src/Spring/Spring.Core/Spring.Core.2003.csproj @@ -2070,11 +2070,6 @@ SubType = "Code" BuildAction = "Compile" /> - + + + diff --git a/src/Spring/Spring.Core/Spring.Core.2008.csproj b/src/Spring/Spring.Core/Spring.Core.2008.csproj index c0a91e23..e780bbd4 100644 --- a/src/Spring/Spring.Core/Spring.Core.2008.csproj +++ b/src/Spring/Spring.Core/Spring.Core.2008.csproj @@ -1113,6 +1113,7 @@ + diff --git a/src/Spring/Spring.Core/Util/IErrorHandler.cs b/src/Spring/Spring.Core/Util/IErrorHandler.cs new file mode 100644 index 00000000..564f3420 --- /dev/null +++ b/src/Spring/Spring.Core/Util/IErrorHandler.cs @@ -0,0 +1,42 @@ +#region License + +/* + * Copyright 2002-2009 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#endregion + +using System; + +namespace Spring.Util +{ + /// + /// A strategy for handling errors. This is especially useful for handling + /// errors that occur during asynchronous execution as in such cases it may not be + /// possible to throw the error to the original caller. + /// + /// Mark Fisher + /// Mark Pollack (.NET) + public interface IErrorHandler + { + /// + /// Handles the error. + /// + /// The exception. + void HandleError(Exception exception); + + } + +} \ No newline at end of file diff --git a/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/MessageListenerContainerObjectDefinitionParser.cs b/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/MessageListenerContainerObjectDefinitionParser.cs index 9fc3a226..bab73576 100644 --- a/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/MessageListenerContainerObjectDefinitionParser.cs +++ b/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/MessageListenerContainerObjectDefinitionParser.cs @@ -94,6 +94,10 @@ namespace Spring.Messaging.Ems.Config private readonly string PUBSUB_DOMAIN_ATTRIBUTE = "pubsub-domain"; private readonly string AUTO_STARTUP = "auto-startup"; + + private readonly string ERROR_HANDLER_ATTRIBUTE = "error-handler"; + + private readonly string EXCEPTION_LISTENER_ATTRIBUTE = "exception-listener"; #endregion @@ -257,11 +261,26 @@ namespace Spring.Messaging.Ems.Config containerDef.AddPropertyValue("ConnectionFactory", new RuntimeObjectReference(connectionFactoryObjectName)); - string destinationResolverBeanName = containerElement.GetAttribute(DESTINATION_RESOLVER_ATTRIBUTE); - if (StringUtils.HasText(destinationResolverBeanName)) + + string errorHandlerObjectName = containerElement.GetAttribute(ERROR_HANDLER_ATTRIBUTE); + if (StringUtils.HasText(errorHandlerObjectName)) + { + containerDef.AddPropertyValue("ErrorHandler", + new RuntimeObjectReference(errorHandlerObjectName)); + } + + string exceptionListenerObjectName = containerElement.GetAttribute(EXCEPTION_LISTENER_ATTRIBUTE); + if (StringUtils.HasText(exceptionListenerObjectName)) + { + containerDef.AddPropertyValue("ExceptionListener", + new RuntimeObjectReference(exceptionListenerObjectName)); + } + + string destinationResolverObjectName = containerElement.GetAttribute(DESTINATION_RESOLVER_ATTRIBUTE); + if (StringUtils.HasText(destinationResolverObjectName)) { containerDef.AddPropertyValue("DestinationResolver", - new RuntimeObjectReference(destinationResolverBeanName)); + new RuntimeObjectReference(destinationResolverObjectName)); } string acknowledge = containerElement.GetAttribute(ACKNOWLEDGE_ATTRIBUTE); diff --git a/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/spring-ems-1.3.xsd b/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/spring-ems-1.3.xsd index 2364c72e..43c8b3b1 100644 --- a/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/spring-ems-1.3.xsd +++ b/src/Spring/Spring.Messaging.Ems/Messaging/Ems/Config/spring-ems-1.3.xsd @@ -83,7 +83,27 @@ ]]> - + + + + + + + + + + + + + + + + /// Sets an ErrorHandler to be invoked in case of any uncaught exceptions thrown + /// while processing a Message. By default there will be no ErrorHandler + /// so that error-level logging is the only result. + /// + /// The error handler. + public IErrorHandler ErrorHandler + { + set { errorHandler = value; } + } + /// /// Gets or sets a value indicating whether to expose listener session to a registered @@ -545,8 +558,8 @@ namespace Spring.Messaging.Ems.Listener if (Active) { // Regular case: failed while active. - // Log at error level. - logger.Error("Execution of EMS message listener failed", ex); + // Invoke ErrorHandler if available. + InvokeErrorHandler(ex); } else { @@ -556,6 +569,18 @@ namespace Spring.Messaging.Ems.Listener } } + protected virtual void InvokeErrorHandler(Exception exception) + { + if (errorHandler != null) + { + errorHandler.HandleError(exception); + } + else if (logger.IsWarnEnabled) + { + logger.Warn("Execution of EMS message listener failed, and no ErrorHandler has been set.", exception); + } + } + /// /// Invokes the registered exception listener, if any. /// diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/MessageListenerContainerObjectDefinitionParser.cs b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/MessageListenerContainerObjectDefinitionParser.cs index 9177578a..e610a8a9 100644 --- a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/MessageListenerContainerObjectDefinitionParser.cs +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/MessageListenerContainerObjectDefinitionParser.cs @@ -21,7 +21,6 @@ using System; using System.Xml; using Apache.NMS; -using Spring.Core.TypeConversion; using Spring.Core.TypeResolution; using Spring.Messaging.Nms.Listener; using Spring.Messaging.Nms.Listener.Adapter; @@ -95,6 +94,10 @@ namespace Spring.Messaging.Nms.Config private readonly string PUBSUB_DOMAIN_ATTRIBUTE = "pubsub-domain"; private readonly string AUTO_STARTUP = "auto-startup"; + + private readonly string ERROR_HANDLER_ATTRIBUTE = "error-handler"; + + private readonly string EXCEPTION_LISTENER_ATTRIBUTE = "exception-listener"; #endregion @@ -260,11 +263,26 @@ namespace Spring.Messaging.Nms.Config containerDef.AddPropertyValue("ConnectionFactory", new RuntimeObjectReference(connectionFactoryObjectName)); - string destinationResolverBeanName = containerElement.GetAttribute(DESTINATION_RESOLVER_ATTRIBUTE); - if (StringUtils.HasText(destinationResolverBeanName)) + + string errorHandlerObjectName = containerElement.GetAttribute(ERROR_HANDLER_ATTRIBUTE); + if (StringUtils.HasText(errorHandlerObjectName)) + { + containerDef.AddPropertyValue("ErrorHandler", + new RuntimeObjectReference(errorHandlerObjectName)); + } + + string exceptionListenerObjectName = containerElement.GetAttribute(EXCEPTION_LISTENER_ATTRIBUTE); + if (StringUtils.HasText(exceptionListenerObjectName)) + { + containerDef.AddPropertyValue("ExceptionListener", + new RuntimeObjectReference(exceptionListenerObjectName)); + } + + string destinationResolverObjectName = containerElement.GetAttribute(DESTINATION_RESOLVER_ATTRIBUTE); + if (StringUtils.HasText(destinationResolverObjectName)) { containerDef.AddPropertyValue("DestinationResolver", - new RuntimeObjectReference(destinationResolverBeanName)); + new RuntimeObjectReference(destinationResolverObjectName)); } string acknowledge = containerElement.GetAttribute(ACKNOWLEDGE_ATTRIBUTE); diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/NmsNamespaceParser.cs b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/NmsNamespaceParser.cs index 13356a91..cee8d733 100644 --- a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/NmsNamespaceParser.cs +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/NmsNamespaceParser.cs @@ -33,7 +33,7 @@ namespace Spring.Messaging.Nms.Config NamespaceParser( Namespace = "http://www.springframework.net/nms", SchemaLocationAssemblyHint = typeof (NmsNamespaceParser), - SchemaLocation = "/Spring.Messaging.Nms.Config/spring-nms-1.2.xsd" + SchemaLocation = "/Spring.Messaging.Nms.Config/spring-nms-1.3.xsd" ) ] public class NmsNamespaceParser : NamespaceParserSupport diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/spring-nms-1.3.xsd b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/spring-nms-1.3.xsd new file mode 100644 index 00000000..92f502c4 --- /dev/null +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Config/spring-nms-1.3.xsd @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/AbstractMessageListenerContainer.cs b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/AbstractMessageListenerContainer.cs index c816fee0..97445009 100644 --- a/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/AbstractMessageListenerContainer.cs +++ b/src/Spring/Spring.Messaging.Nms/Messaging/Nms/Listener/AbstractMessageListenerContainer.cs @@ -56,6 +56,8 @@ namespace Spring.Messaging.Nms.Listener private IExceptionListener exceptionListener; + private IErrorHandler errorHandler; + private bool exposeListenerISession = true; private bool acceptMessagesWhileStopping = false; @@ -208,6 +210,17 @@ namespace Spring.Messaging.Nms.Listener set { exceptionListener = value; } } + /// + /// Sets an ErrorHandler to be invoked in case of any uncaught exceptions thrown + /// while processing a Message. By default there will be no ErrorHandler + /// so that error-level logging is the only result. + /// + /// The error handler. + public IErrorHandler ErrorHandler + { + set { errorHandler = value; } + } + /// /// Gets or sets a value indicating whether to expose listener session to a registered @@ -544,8 +557,8 @@ namespace Spring.Messaging.Nms.Listener if (Active) { // Regular case: failed while active. - // Log at error level. - logger.Error("Execution of NMS message listener failed", ex); + // Invoke ErrorHandler if available. + InvokeErrorHandler(ex); } else { @@ -555,6 +568,22 @@ namespace Spring.Messaging.Nms.Listener } } + /// + /// Invokes the error handler. + /// + /// The exception. + protected virtual void InvokeErrorHandler(Exception exception) + { + if (errorHandler != null) + { + errorHandler.HandleError(exception); + } + else if(logger.IsWarnEnabled) + { + logger.Warn("Execution of NMS message listener failed, and no ErrorHandler has been set.", exception); + } + } + /// /// Invokes the registered exception listener, if any. /// @@ -568,6 +597,8 @@ namespace Spring.Messaging.Nms.Listener exListener.OnException(ex); } } + + #endregion diff --git a/src/Spring/Spring.Messaging.Nms/Spring.Messaging.Nms.2008.csproj b/src/Spring/Spring.Messaging.Nms/Spring.Messaging.Nms.2008.csproj index 035ee761..4f46c357 100644 --- a/src/Spring/Spring.Messaging.Nms/Spring.Messaging.Nms.2008.csproj +++ b/src/Spring/Spring.Messaging.Nms/Spring.Messaging.Nms.2008.csproj @@ -119,6 +119,9 @@ + + + diff --git a/test/Spring/Spring.Messaging.Ems.Tests/Messaging/Ems/Core/SimpleMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Ems.Tests/Messaging/Ems/Core/SimpleMessageListenerContainerTests.cs index 47c0855c..6156fb92 100644 --- a/test/Spring/Spring.Messaging.Ems.Tests/Messaging/Ems/Core/SimpleMessageListenerContainerTests.cs +++ b/test/Spring/Spring.Messaging.Ems.Tests/Messaging/Ems/Core/SimpleMessageListenerContainerTests.cs @@ -26,6 +26,7 @@ using NUnit.Framework; using Rhino.Mocks; using Spring.Messaging.Ems.Common; using Spring.Messaging.Ems.Listener; +using Spring.Util; using TIBCO.EMS; #endregion @@ -99,17 +100,56 @@ namespace Spring.Messaging.Ems.Core // manually trigger an Exception with the above bad MessageListener... messageConsumer.SendMessage(message); + mocks.VerifyAll(); + } + [Test] + public void RegisteredErrorHandlerIsInvokedOnException() + { + SimpleMessageConsumer messageConsumer = new SimpleMessageConsumer(); + + ISession session = (ISession)mocks.CreateMock(typeof(ISession)); + Expect.Call(session.CreateQueue(DESTINATION_NAME)).Return(QUEUE_DESTINATION); + Expect.Call(session.CreateConsumer(QUEUE_DESTINATION, null)).Return(messageConsumer); + // an exception is thrown, so the rollback logic is being applied here... + Expect.Call(session.Transacted).Return(false); + + IConnection connection = (IConnection)mocks.CreateMock(typeof(IConnection)); + //connection.ExceptionListener += container.OnException; + connection.ExceptionListener = container; + Expect.Call(connection.CreateSession(false, container.SessionAcknowledgeMode)).Return(session); + connection.Start(); + + IConnectionFactory connectionFactory = (IConnectionFactory)mocks.CreateMock(typeof(IConnectionFactory)); + Expect.Call(connectionFactory.CreateConnection()).Return(connection); + + IllegalStateException theException = new IllegalStateException(EXCEPTION_MESSAGE); + + IErrorHandler errorHandler = (IErrorHandler)mocks.CreateMock(typeof(IErrorHandler)); + errorHandler.HandleError(theException); + + //IMessage message = (IMessage) mocks.CreateMock(typeof (IMessage)); + TextMessage message = new TextMessage(null, "hello"); + + + mocks.ReplayAll(); + + + container.ConnectionFactory = connectionFactory; + container.DestinationName = DESTINATION_NAME; + container.MessageListener = new BadSessionAwareMessageListener(theException); + container.ErrorHandler = errorHandler; + container.AfterPropertiesSet(); + + // manually trigger an Exception with the above bad MessageListener... + messageConsumer.SendMessage(message); mocks.VerifyAll(); - - - - - } } + + internal class BadSessionAwareMessageListener : ISessionAwareMessageListener { private EMSException exception; diff --git a/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Core/SimpleMessageListenerContainerTests.cs b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Core/SimpleMessageListenerContainerTests.cs index e20b0a09..5e414e11 100644 --- a/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Core/SimpleMessageListenerContainerTests.cs +++ b/test/Spring/Spring.Messaging.Nms.Tests/Messaging/Nms/Core/SimpleMessageListenerContainerTests.cs @@ -25,6 +25,7 @@ using Apache.NMS; using NUnit.Framework; using Rhino.Mocks; using Spring.Messaging.Nms.Listener; +using Spring.Util; #endregion @@ -94,15 +95,48 @@ namespace Spring.Messaging.Nms.Core // manually trigger an Exception with the above bad MessageListener... messageConsumer.SendMessage(message); + mocks.VerifyAll(); + } + + [Test] + public void RegisteredErrorHandlerIsInvokedOnException() + { + SimpleMessageConsumer messageConsumer = new SimpleMessageConsumer(); + + ISession session = (ISession)mocks.CreateMock(typeof(ISession)); + Expect.Call(session.GetQueue(DESTINATION_NAME)).Return(QUEUE_DESTINATION); + Expect.Call(session.CreateConsumer(QUEUE_DESTINATION, null)).Return(messageConsumer); + // an exception is thrown, so the rollback logic is being applied here... + Expect.Call(session.Transacted).Return(false); + + IConnection connection = (IConnection)mocks.CreateMock(typeof(IConnection)); + connection.ExceptionListener += container.OnException; + Expect.Call(connection.CreateSession(container.SessionAcknowledgeMode)).Return(session); + connection.Start(); + + IConnectionFactory connectionFactory = (IConnectionFactory)mocks.CreateMock(typeof(IConnectionFactory)); + Expect.Call(connectionFactory.CreateConnection()).Return(connection); + + IllegalStateException theException = new IllegalStateException(EXCEPTION_MESSAGE); + + IErrorHandler errorHandler = (IErrorHandler)mocks.CreateMock(typeof(IErrorHandler)); + errorHandler.HandleError(theException); + + IMessage message = (IMessage)mocks.CreateMock(typeof(IMessage)); + + mocks.ReplayAll(); + container.ConnectionFactory = connectionFactory; + container.DestinationName = DESTINATION_NAME; + container.MessageListener = new BadSessionAwareMessageListener(theException); + container.ErrorHandler = errorHandler; + container.AfterPropertiesSet(); + + // manually trigger an Exception with the above bad MessageListener... + messageConsumer.SendMessage(message); mocks.VerifyAll(); - - - - - } }