From 89d63eb79b61764672e9ef9b3926e3635bbb188a Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 29 Jul 2014 12:29:24 +0200 Subject: [PATCH] JmsMessagingTemplate exception management This commit introduces MessagingExceptionTranslator, a messaging exception translation infrastructure similar to what PersistenceExceptionTranslator provides. JmsMessagingTemplate does not throw raw JmsException anymore but translates those to an instance of Spring's MessagingException hierarchy. Issue: SPR-12038 --- .../core/JmsMessagingExceptionTranslator.java | 56 +++++++++ .../jms/core/JmsMessagingTemplate.java | 87 +++++++++++--- .../JmsMessagingExceptionTranslatorTests.java | 34 ++++++ .../jms/core/JmsMessagingTemplateTests.java | 113 +++++++++++++++++- .../MessagingExceptionTranslator.java | 44 +++++++ 5 files changed, 319 insertions(+), 15 deletions(-) create mode 100644 spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingExceptionTranslator.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingExceptionTranslatorTests.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/MessagingExceptionTranslator.java diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingExceptionTranslator.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingExceptionTranslator.java new file mode 100644 index 0000000000..01e3f49662 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingExceptionTranslator.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2014 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. + */ + +package org.springframework.jms.core; + +import org.springframework.jms.InvalidDestinationException; +import org.springframework.jms.JmsException; +import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.destination.DestinationResolutionException; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.MessagingExceptionTranslator; + +/** + * {@link MessagingExceptionTranslator} capable of translating {@link JmsException} + * instances to Spring's {@link MessagingException} hierarchy. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class JmsMessagingExceptionTranslator implements MessagingExceptionTranslator { + + @Override + public MessagingException translateExceptionIfPossible(RuntimeException ex) { + if (ex instanceof JmsException) { + return convertJmsException((JmsException) ex); + } + return null; + } + + private MessagingException convertJmsException(JmsException ex) { + if (ex instanceof DestinationResolutionException || + ex instanceof InvalidDestinationException) { + return new org.springframework.messaging.core.DestinationResolutionException(ex.getMessage(), ex); + } + if (ex instanceof MessageConversionException) { + return new org.springframework.messaging.converter.MessageConversionException(ex.getMessage(), ex); + } + + + // Fallback + return new MessagingException(ex.getMessage(), ex); + } +} diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java index d4089c0ba4..a1b42b7833 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java @@ -22,12 +22,14 @@ import javax.jms.JMSException; import javax.jms.Session; import org.springframework.beans.factory.InitializingBean; -import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.JmsException; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.MessagingMessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; +import org.springframework.messaging.MessagingExceptionTranslator; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.core.AbstractMessagingTemplate; import org.springframework.messaging.core.MessagePostProcessor; import org.springframework.util.Assert; @@ -45,6 +47,8 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate private MessageConverter jmsMessageConverter = new MessagingMessageConverter(); + private MessagingExceptionTranslator exceptionTranslator = new JmsMessagingExceptionTranslator(); + private String defaultDestinationName; @@ -85,6 +89,14 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate this.jmsMessageConverter = jmsMessageConverter; } + /** + * Set the {@link MessagingExceptionTranslator} to use. Default to + * {@link JmsMessagingExceptionTranslator}. + */ + public void setExceptionTranslator(MessagingExceptionTranslator exceptionTranslator) { + this.exceptionTranslator = exceptionTranslator; + } + /** * Configure the default destination name to use in send methods that don't have * a destination argument. If a default destination is not configured, send methods @@ -271,35 +283,65 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate @Override protected void doSend(Destination destination, Message message) { - this.jmsTemplate.send(destination, createMessageCreator(message)); + try { + this.jmsTemplate.send(destination, createMessageCreator(message)); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } protected void doSend(String destinationName, Message message) { - this.jmsTemplate.send(destinationName, createMessageCreator(message)); + try { + this.jmsTemplate.send(destinationName, createMessageCreator(message)); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } @Override protected Message doReceive(Destination destination) { - javax.jms.Message jmsMessage = this.jmsTemplate.receive(destination); - return doConvert(jmsMessage); + try { + javax.jms.Message jmsMessage = this.jmsTemplate.receive(destination); + return doConvert(jmsMessage); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } protected Message doReceive(String destinationName) { - javax.jms.Message jmsMessage = this.jmsTemplate.receive(destinationName); - return doConvert(jmsMessage); + try { + javax.jms.Message jmsMessage = this.jmsTemplate.receive(destinationName); + return doConvert(jmsMessage); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } @Override protected Message doSendAndReceive(Destination destination, Message requestMessage) { - javax.jms.Message jmsMessage = this.jmsTemplate - .sendAndReceive(destination, createMessageCreator(requestMessage)); - return doConvert(jmsMessage); + try { + javax.jms.Message jmsMessage = this.jmsTemplate + .sendAndReceive(destination, createMessageCreator(requestMessage)); + return doConvert(jmsMessage); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } protected Message doSendAndReceive(String destinationName, Message requestMessage) { - javax.jms.Message jmsMessage = this.jmsTemplate - .sendAndReceive(destinationName, createMessageCreator(requestMessage)); - return doConvert(jmsMessage); + try { + javax.jms.Message jmsMessage = this.jmsTemplate + .sendAndReceive(destinationName, createMessageCreator(requestMessage)); + return doConvert(jmsMessage); + } + catch (JmsException ex) { + throw translateIfNecessary(ex); + } } private MessagingMessageCreator createMessageCreator(Message message) { @@ -325,6 +367,15 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate catch (JMSException ex) { throw new MessageConversionException("Could not convert '" + message + "'", ex); } + catch (JmsException ex) { + throw new MessageConversionException("Could not convert '" + message + "'", ex); + } + } + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + protected RuntimeException translateIfNecessary(RuntimeException rawException) { + MessagingException messagingException = this.exceptionTranslator.translateExceptionIfPossible(rawException); + return (messagingException != null ? messagingException : rawException); } @@ -341,7 +392,15 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate @Override public javax.jms.Message createMessage(Session session) throws JMSException { - return this.messageConverter.toMessage(this.message, session); + try { + return this.messageConverter.toMessage(this.message, session); + } + catch (JMSException ex) { + throw new MessageConversionException("Could not convert '" + message + "'", ex); + } + catch (JmsException ex) { + throw new MessageConversionException("Could not convert '" + message + "'", ex); + } } } diff --git a/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingExceptionTranslatorTests.java b/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingExceptionTranslatorTests.java new file mode 100644 index 0000000000..b9a71931a6 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingExceptionTranslatorTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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. + */ + +package org.springframework.jms.core; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Stephane Nicoll + */ +public class JmsMessagingExceptionTranslatorTests { + + private final JmsMessagingExceptionTranslator translator = new JmsMessagingExceptionTranslator(); + + @Test + public void translateNonJmsException() { + assertNull(translator.translateExceptionIfPossible(new NullPointerException())); + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingTemplateTests.java b/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingTemplateTests.java index ad3cff425a..c4617f9c00 100644 --- a/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingTemplateTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/core/JmsMessagingTemplateTests.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.Map; import javax.jms.Destination; import javax.jms.JMSException; +import javax.jms.MessageFormatException; +import javax.jms.MessageNotWriteableException; import javax.jms.Session; import javax.jms.TextMessage; @@ -36,11 +38,16 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import org.springframework.jms.InvalidDestinationException; +import org.springframework.jms.MessageNotReadableException; import org.springframework.jms.StubTextMessage; -import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.jms.support.destination.DestinationResolutionException; import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; import org.springframework.messaging.converter.GenericMessageConverter; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.support.MessageBuilder; import static org.junit.Assert.*; @@ -443,6 +450,110 @@ public class JmsMessagingTemplateTests { messagingTemplate.convertSendAndReceive("my Payload", String.class); } + @Test + public void convertMessageConversionExceptionOnSend() throws JMSException { + Message message = createTextMessage(); + MessageConverter messageConverter = mock(MessageConverter.class); + doThrow(org.springframework.jms.support.converter.MessageConversionException.class) + .when(messageConverter).toMessage(eq(message), anyObject()); + messagingTemplate.setJmsMessageConverter(messageConverter); + invokeMessageCreator("myQueue"); + + thrown.expect(org.springframework.messaging.converter.MessageConversionException.class); + messagingTemplate.send("myQueue", message); + } + + @Test + public void convertMessageConversionExceptionOnReceive() throws JMSException { + javax.jms.Message message = createJmsTextMessage(); + MessageConverter messageConverter = mock(MessageConverter.class); + doThrow(org.springframework.jms.support.converter.MessageConversionException.class) + .when(messageConverter).fromMessage(message); + messagingTemplate.setJmsMessageConverter(messageConverter); + given(jmsTemplate.receive("myQueue")).willReturn(message); + + thrown.expect(org.springframework.messaging.converter.MessageConversionException.class); + messagingTemplate.receive("myQueue"); + } + + @Test + public void convertMessageNotReadableException() throws JMSException { + doThrow(MessageNotReadableException.class).when(jmsTemplate).receive("myQueue"); + + thrown.expect(MessagingException.class); + messagingTemplate.receive("myQueue"); + } + + @Test + public void convertDestinationResolutionExceptionOnSend() { + Destination destination = new Destination() {}; + doThrow(DestinationResolutionException.class).when(jmsTemplate).send(eq(destination), anyObject()); + + thrown.expect(org.springframework.messaging.core.DestinationResolutionException.class); + messagingTemplate.send(destination, createTextMessage()); + } + + @Test + public void convertDestinationResolutionExceptionOnReceive() { + Destination destination = new Destination() {}; + doThrow(DestinationResolutionException.class).when(jmsTemplate).receive(destination); + + thrown.expect(org.springframework.messaging.core.DestinationResolutionException.class); + messagingTemplate.receive(destination); + } + + @Test + public void convertMessageFormatException() throws JMSException { + Message message = createTextMessage(); + MessageConverter messageConverter = mock(MessageConverter.class); + doThrow(MessageFormatException.class).when(messageConverter).toMessage(eq(message), anyObject()); + messagingTemplate.setJmsMessageConverter(messageConverter); + invokeMessageCreator("myQueue"); + + thrown.expect(org.springframework.messaging.converter.MessageConversionException.class); + messagingTemplate.send("myQueue", message); + } + + @Test + public void convertMessageNotWritableException() throws JMSException { + Message message = createTextMessage(); + MessageConverter messageConverter = mock(MessageConverter.class); + doThrow(MessageNotWriteableException.class).when(messageConverter).toMessage(eq(message), anyObject()); + messagingTemplate.setJmsMessageConverter(messageConverter); + invokeMessageCreator("myQueue"); + + thrown.expect(org.springframework.messaging.converter.MessageConversionException.class); + messagingTemplate.send("myQueue", message); + } + + @Test + public void convertInvalidDestinationExceptionOnSendAndReceiveWithName() { + doThrow(InvalidDestinationException.class).when(jmsTemplate).sendAndReceive(eq("unknownQueue"), anyObject()); + + thrown.expect(org.springframework.messaging.core.DestinationResolutionException.class); + messagingTemplate.sendAndReceive("unknownQueue", createTextMessage()); + } + + @Test + public void convertInvalidDestinationExceptionOnSendAndReceive() { + Destination destination = new Destination() {}; + doThrow(InvalidDestinationException.class).when(jmsTemplate).sendAndReceive(eq(destination), anyObject()); + + thrown.expect(org.springframework.messaging.core.DestinationResolutionException.class); + messagingTemplate.sendAndReceive(destination, createTextMessage()); + } + + private void invokeMessageCreator(String destinationName) { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + MessageCreator messageCreator = (MessageCreator) invocation.getArguments()[1]; + messageCreator.createMessage(null); + return null; + } + }).when(jmsTemplate).send(eq("myQueue"), anyObject()); + } + private Message createTextMessage(String payload) { return MessageBuilder diff --git a/spring-messaging/src/main/java/org/springframework/messaging/MessagingExceptionTranslator.java b/spring-messaging/src/main/java/org/springframework/messaging/MessagingExceptionTranslator.java new file mode 100644 index 0000000000..0dc5fa026b --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessagingExceptionTranslator.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2014 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. + */ + +package org.springframework.messaging; + +/** + * Interface implemented by Spring integrations with messaging technologies + * that throw runtime exceptions, such as JMS, STOMP and AMQP. + * + *

This allows consistent usage of combined exception translation functionality, + * without forcing a single translator to understand every single possible type + * of exception. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface MessagingExceptionTranslator { + + /** + * Translate the given runtime exception thrown by a messaging implementation + * to a corresponding exception from Spring's generic {@link MessagingException} + * hierarchy, if possible. + *

Do not translate exceptions that are not understand by this translator: + * for example, if resulting from user code and unrelated to messaging. + * @param ex a RuntimeException thrown + * @return the corresponding MessagingException (or {@code null} if the + * exception could not be translated, as in this case it may result from + * user code rather than an actual messaging problem) + */ + MessagingException translateExceptionIfPossible(RuntimeException ex); +}