diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java index fa1296a31e7..072d3e29cdb 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java @@ -235,10 +235,10 @@ public abstract class AbstractAdaptableMessageListener * Extract the message body from the given JMS message. * @param message the JMS {@code Message} * @return the content of the message, to be passed into the listener method - * as an argument + * as an argument, or {@code null} if the message has no body * @throws MessageConversionException if the message could not be extracted */ - protected Object extractMessage(Message message) { + protected @Nullable Object extractMessage(Message message) { try { MessageConverter converter = getMessageConverter(); if (converter != null) { @@ -462,7 +462,7 @@ public abstract class AbstractAdaptableMessageListener } @Override - protected Object extractPayload(Message message) throws JMSException { + protected @Nullable Object extractPayload(Message message) throws JMSException { Object payload = extractMessage(message); if (message instanceof BytesMessage bytesMessage) { try { @@ -532,6 +532,10 @@ public abstract class AbstractAdaptableMessageListener if (payload instanceof org.springframework.messaging.Message springMessage) { return springMessage.getPayload(); } + if (payload == null) { + throw new MessageConversionException( + "Cannot convert JMS message with no body to a Message with a payload: " + this.message); + } return payload; } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java index b8a02946182..6c387f4f381 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java @@ -106,9 +106,9 @@ import org.springframework.util.ObjectUtils; * String handleMessage(String text); * } * - * For further examples and discussion please do refer to the Spring - * reference documentation which describes this class (and it's attendant - * XML configuration) in detail. + *

For further examples and discussion, please refer to the Spring reference + * documentation which describes this class (and its corresponding configuration) + * in detail. * * @author Juergen Hoeller * @since 2.0 @@ -219,7 +219,7 @@ public class MessageListenerAdapter extends AbstractAdaptableMessageListener imp String methodName = getListenerMethodName(message, convertedMessage); // Invoke the handler method with appropriate arguments. - Object[] listenerArguments = buildListenerArguments(convertedMessage); + @Nullable Object[] listenerArguments = buildListenerArguments(convertedMessage); Object result = invokeListenerMethod(methodName, listenerArguments); if (result != null) { handleResult(result, message, session); @@ -247,12 +247,12 @@ public class MessageListenerAdapter extends AbstractAdaptableMessageListener imp * default listener method, if any. * @param originalMessage the JMS request message * @param extractedMessage the converted JMS request message, - * to be passed into the listener method as argument + * to be passed into the listener method as an argument * @return the name of the listener method (never {@code null}) * @throws JMSException if thrown by JMS API methods * @see #setDefaultListenerMethod */ - protected String getListenerMethodName(Message originalMessage, Object extractedMessage) throws JMSException { + protected String getListenerMethodName(Message originalMessage, @Nullable Object extractedMessage) throws JMSException { return getDefaultListenerMethod(); } @@ -260,31 +260,34 @@ public class MessageListenerAdapter extends AbstractAdaptableMessageListener imp * Build an array of arguments to be passed into the target listener method. * Allows for multiple method arguments to be built from a single message object. *

The default implementation builds an array with the given message object - * as sole element. This means that the extracted message will always be passed + * as its sole element. This means that the extracted message will always be passed * into a single method argument, even if it is an array, with the target * method having a corresponding single argument of the array's type declared. *

This can be overridden to treat special message content such as arrays * differently, for example passing in each element of the message array - * as distinct method argument. - * @param extractedMessage the content of the message + * as a distinct method argument. + * @param extractedMessage the content of the message, which may be + * {@code null} if the message had no body * @return the array of arguments to be passed into the * listener method (each element of the array corresponding - * to a distinct method argument) + * to a distinct method argument); an element may be {@code null} + * if the corresponding extracted message content was {@code null} */ - protected Object[] buildListenerArguments(Object extractedMessage) { + protected @Nullable Object[] buildListenerArguments(@Nullable Object extractedMessage) { return new Object[] {extractedMessage}; } /** * Invoke the specified listener method. * @param methodName the name of the listener method - * @param arguments the message arguments to be passed in + * @param arguments the message arguments to be passed in (an element + * may be {@code null}) * @return the result returned from the listener method * @throws JMSException if thrown by JMS API methods * @see #getListenerMethodName * @see #buildListenerArguments */ - protected @Nullable Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException { + protected @Nullable Object invokeListenerMethod(String methodName, @Nullable Object[] arguments) throws JMSException { try { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(getDelegate()); diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java index 6ecd85bd8bf..e9b4dc769f6 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java @@ -19,6 +19,7 @@ package org.springframework.jms.support.converter; import jakarta.jms.JMSException; import jakarta.jms.Message; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; /** * Strategy interface that specifies a converter between Java objects and JMS messages. @@ -48,10 +49,12 @@ public interface MessageConverter { /** * Convert from a JMS Message to a Java object. * @param message the message to convert - * @return the converted Java object + * @return the converted Java object, or {@code null} if the message has no + * body (for example, a {@link jakarta.jms.TextMessage} or + * {@link jakarta.jms.ObjectMessage} whose body was never set) * @throws jakarta.jms.JMSException if thrown by JMS API methods * @throws MessageConversionException in case of conversion failure */ - Object fromMessage(Message message) throws JMSException, MessageConversionException; + @Nullable Object fromMessage(Message message) throws JMSException, MessageConversionException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java index 0b422ee5693..7869eac9fcc 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessagingMessageConverter.java @@ -117,6 +117,9 @@ public class MessagingMessageConverter implements MessageConverter, Initializing public Object fromMessage(jakarta.jms.Message message) throws JMSException, MessageConversionException { Map mappedHeaders = extractHeaders(message); Object convertedObject = extractPayload(message); + if (convertedObject == null) { + throw new MessageConversionException("Cannot convert JMS message with no body to a Message with a payload"); + } MessageBuilder builder = (convertedObject instanceof org.springframework.messaging.Message springMessage ? MessageBuilder.fromMessage(springMessage) : MessageBuilder.withPayload(convertedObject)); return builder.copyHeadersIfAbsent(mappedHeaders).build(); @@ -124,8 +127,9 @@ public class MessagingMessageConverter implements MessageConverter, Initializing /** * Extract the payload of the specified {@link jakarta.jms.Message}. + * @return the extracted payload, or {@code null} if the message has no body */ - protected Object extractPayload(jakarta.jms.Message message) throws JMSException { + protected @Nullable Object extractPayload(jakarta.jms.Message message) throws JMSException { return this.payloadConverter.fromMessage(message); } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java index e51e2cf8826..ee3cfc6a043 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java @@ -28,6 +28,7 @@ import jakarta.jms.Message; import jakarta.jms.ObjectMessage; import jakarta.jms.Session; import jakarta.jms.TextMessage; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; @@ -91,13 +92,15 @@ public class SimpleMessageConverter implements MessageConverter { * ByteMessage back to a byte array, a MapMessage back to a Map, * and an ObjectMessage back to a Serializable object. Returns * the plain Message object in case of an unknown message type. + *

Note that a {@code TextMessage} or {@code ObjectMessage} whose body was + * never set is converted to {@code null}, per the JMS specification. * @see #extractStringFromMessage * @see #extractByteArrayFromMessage * @see #extractMapFromMessage * @see #extractSerializableFromMessage */ @Override - public Object fromMessage(Message message) throws JMSException, MessageConversionException { + public @Nullable Object fromMessage(Message message) throws JMSException, MessageConversionException { if (message instanceof TextMessage textMessage) { return extractStringFromMessage(textMessage); } @@ -179,10 +182,10 @@ public class SimpleMessageConverter implements MessageConverter { /** * Extract a String from the given TextMessage. * @param message the message to convert - * @return the resulting String + * @return the resulting String, or {@code null} if the message body was never set * @throws JMSException if thrown by JMS methods */ - protected String extractStringFromMessage(TextMessage message) throws JMSException { + protected @Nullable String extractStringFromMessage(TextMessage message) throws JMSException { return message.getText(); } @@ -218,10 +221,11 @@ public class SimpleMessageConverter implements MessageConverter { /** * Extract a Serializable object from the given {@link ObjectMessage}. * @param message the message to convert - * @return the resulting Serializable object + * @return the resulting Serializable object, or {@code null} if the message body + * was never set * @throws JMSException if thrown by JMS methods */ - protected Serializable extractSerializableFromMessage(ObjectMessage message) throws JMSException { + protected @Nullable Serializable extractSerializableFromMessage(ObjectMessage message) throws JMSException { return message.getObject(); } diff --git a/spring-jms/src/test/java/org/springframework/jms/support/SimpleMessageConverterTests.java b/spring-jms/src/test/java/org/springframework/jms/support/SimpleMessageConverterTests.java index 37a1ffb435b..217f17fc4b7 100644 --- a/spring-jms/src/test/java/org/springframework/jms/support/SimpleMessageConverterTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/support/SimpleMessageConverterTests.java @@ -64,6 +64,15 @@ class SimpleMessageConverterTests { assertThat(converter.fromMessage(msg)).isEqualTo(content); } + @Test // gh-37148 + void stringConversionForMessageWithNoTextReturnsNull() throws JMSException { + TextMessage message = mock(); + given(message.getText()).willReturn(null); + + SimpleMessageConverter converter = new SimpleMessageConverter(); + assertThat(converter.fromMessage(message)).isNull(); + } + @Test void byteArrayConversion() throws JMSException { Session session = mock(); @@ -121,6 +130,15 @@ class SimpleMessageConverterTests { assertThat(converter.fromMessage(msg)).isEqualTo(content); } + @Test // gh-37148 + void serializableConversionForMessageWithNoObjectReturnsNull() throws JMSException { + ObjectMessage message = mock(); + given(message.getObject()).willReturn(null); + + SimpleMessageConverter converter = new SimpleMessageConverter(); + assertThat(converter.fromMessage(message)).isNull(); + } + @Test void toMessageThrowsExceptionIfGivenNullObjectToConvert() { assertThatExceptionOfType(MessageConversionException.class).isThrownBy(() ->