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 10ae1cf820..57a1a53088 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 @@ -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. @@ -36,6 +36,7 @@ import org.springframework.jms.support.converter.MessageConversionException; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.MessagingMessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.jms.support.converter.SmartMessageConverter; import org.springframework.jms.support.destination.DestinationResolver; import org.springframework.jms.support.destination.DynamicDestinationResolver; import org.springframework.util.Assert; @@ -269,14 +270,17 @@ public abstract class AbstractAdaptableMessageListener * @see #setMessageConverter */ protected Message buildMessage(Session session, Object result) throws JMSException { - Object content = (result instanceof JmsResponse ? ((JmsResponse>) result).getResponse() : result); - if (content instanceof org.springframework.messaging.Message) { - return this.messagingMessageConverter.toMessage(content, session); - } + Object content = preProcessResponse(result instanceof JmsResponse + ? ((JmsResponse>) result).getResponse() : result); MessageConverter converter = getMessageConverter(); if (converter != null) { - return converter.toMessage(content, session); + if (content instanceof org.springframework.messaging.Message) { + return this.messagingMessageConverter.toMessage(content, session); + } + else { + return converter.toMessage(content, session); + } } if (!(content instanceof Message)) { @@ -286,6 +290,17 @@ public abstract class AbstractAdaptableMessageListener return (Message) content; } + /** + * Pre-process the given result before it is converted to a {@link Message}. + * @param result the result of the invocation + * @return the payload response to handle, either the {@code result} argument or any other + * object (for instance wrapping the result). + * @since 4.3 + */ + protected Object preProcessResponse(Object result) { + return result; + } + /** * Post-process the given response message before it will be sent. *
The default implementation sets the response's correlation id @@ -425,12 +440,17 @@ public abstract class AbstractAdaptableMessageListener } @Override - protected Message createMessageForPayload(Object payload, Session session) throws JMSException { + protected Message createMessageForPayload(Object payload, Session session, Object conversionHint) + throws JMSException { MessageConverter converter = getMessageConverter(); - if (converter != null) { - return converter.toMessage(payload, session); + if (converter == null) { + throw new IllegalStateException("No message converter, cannot handle '" + payload + "'"); + } + if (converter instanceof SmartMessageConverter) { + return ((SmartMessageConverter) converter).toMessage(payload, session, conversionHint); + } - throw new IllegalStateException("No message converter - cannot handle [" + payload + "]"); + return converter.toMessage(payload, session); } } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java index 0791f4c400..b87dd6b236 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.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,11 +19,14 @@ package org.springframework.jms.listener.adapter; import javax.jms.JMSException; import javax.jms.Session; +import org.springframework.core.MethodParameter; import org.springframework.jms.support.JmsHeaderMapper; import org.springframework.jms.support.converter.MessageConversionException; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; +import org.springframework.messaging.core.AbstractMessageSendingTemplate; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.messaging.support.MessageBuilder; /** * A {@link javax.jms.MessageListener} adapter that invokes a configurable @@ -72,6 +75,17 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis } } + @Override + protected Object preProcessResponse(Object result) { + MethodParameter returnType = this.handlerMethod.getReturnType(); + if (result instanceof Message) { + return MessageBuilder.fromMessage((Message>) result) + .setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build(); + } + return MessageBuilder.withPayload(result).setHeader( + AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build(); + } + protected Message> toMessagingMessage(javax.jms.Message jmsMessage) { try { return (Message>) getMessagingMessageConverter().fromMessage(jmsMessage); diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java index c9805aedf4..c6fc97cdd7 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MappingJackson2MessageConverter.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. @@ -29,12 +29,15 @@ import javax.jms.Message; import javax.jms.Session; import javax.jms.TextMessage; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.core.MethodParameter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -55,9 +58,10 @@ import org.springframework.util.ClassUtils; * @author Mark Pollack * @author Dave Syer * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.1.4 */ -public class MappingJackson2MessageConverter implements MessageConverter, BeanClassLoaderAware { +public class MappingJackson2MessageConverter implements SmartMessageConverter, BeanClassLoaderAware { /** * The default encoding used for writing to text messages: UTF-8. @@ -189,6 +193,33 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl return message; } + @Override + public Message toMessage(Object object, Session session, Object conversionHint) + throws JMSException, MessageConversionException { + return toMessage(object, session, getSerializationView(conversionHint)); + } + + /** + * Convert a Java object to a JMS Message using the specified json view + * and the supplied session to create the message object. + * @param object the object to convert + * @param session the Session to use for creating a JMS Message + * @param jsonView the view to use to filter the content + * @return the JMS Message + * @throws javax.jms.JMSException if thrown by JMS API methods + * @throws MessageConversionException in case of conversion failure + * @since 4.3 + */ + public Message toMessage(Object object, Session session, Class> jsonView) + throws JMSException, MessageConversionException { + if (jsonView != null) { + return toMessage(object, session, this.objectMapper.writerWithView(jsonView)); + } + else { + return toMessage(object, session, this.objectMapper.writer()); + } + } + @Override public Object fromMessage(Message message) throws JMSException, MessageConversionException { try { @@ -200,6 +231,28 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl } } + protected Message toMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, MessageConversionException { + Message message; + try { + switch (this.targetType) { + case TEXT: + message = mapToTextMessage(object, session, objectWriter); + break; + case BYTES: + message = mapToBytesMessage(object, session, objectWriter); + break; + default: + message = mapToMessage(object, session, objectWriter, this.targetType); + } + } + catch (IOException ex) { + throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); + } + setTypeIdOnMessage(object, message); + return message; + } + /** * Map the given object to a {@link TextMessage}. @@ -210,12 +263,31 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl * @throws JMSException if thrown by JMS methods * @throws IOException in case of I/O errors * @see Session#createBytesMessage + * @deprecated as of 4.3, use {@link #mapToTextMessage(Object, Session, ObjectWriter)} */ + @Deprecated protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) throws JMSException, IOException { + return mapToTextMessage(object, session, objectMapper.writer()); + } + + /** + * Map the given object to a {@link TextMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectWriter the writer to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + * @since 4.3 + */ + protected TextMessage mapToTextMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, IOException { + StringWriter writer = new StringWriter(); - objectMapper.writeValue(writer, object); + objectWriter.writeValue(writer, object); return session.createTextMessage(writer.toString()); } @@ -228,13 +300,33 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl * @throws JMSException if thrown by JMS methods * @throws IOException in case of I/O errors * @see Session#createBytesMessage + * @deprecated as of 4.3, use {@link #mapToBytesMessage(Object, Session, ObjectWriter)} */ + @Deprecated protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper) throws JMSException, IOException { + return mapToBytesMessage(object, session, objectMapper.writer()); + } + + + /** + * Map the given object to a {@link BytesMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectWriter the writer to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + * @since 4.3 + */ + protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectWriter objectWriter) + throws JMSException, IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding); - objectMapper.writeValue(writer, object); + objectWriter.writeValue(writer, object); BytesMessage message = session.createBytesMessage(); message.writeBytes(bos.toByteArray()); @@ -256,10 +348,31 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl * @return the resulting message * @throws JMSException if thrown by JMS methods * @throws IOException in case of I/O errors + * @deprecated as of 4.3, use {@link #mapToMessage(Object, Session, ObjectWriter, MessageType)} */ + @Deprecated protected Message mapToMessage(Object object, Session session, ObjectMapper objectMapper, MessageType targetType) throws JMSException, IOException { + return mapToMessage(object, session, objectMapper.writer(), targetType); + } + + /** + * Template method that allows for custom message mapping. + * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or + * {@link MessageType#BYTES}. + *
The default implementation throws an {@link IllegalArgumentException}. + * @param object the object to marshal + * @param session the JMS Session + * @param objectWriter the writer to use + * @param targetType the target message type (other than TEXT or BYTES) + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + */ + protected Message mapToMessage(Object object, Session session, ObjectWriter objectWriter, MessageType targetType) + throws JMSException, IOException { + throw new IllegalArgumentException("Unsupported message type [" + targetType + "]. MappingJackson2MessageConverter by default only supports TextMessages and BytesMessages."); } @@ -391,4 +504,42 @@ public class MappingJackson2MessageConverter implements MessageConverter, BeanCl } } + /** + * Determine a Jackson serialization view based on the given conversion hint. + * @param conversionHint the conversion hint Object as passed into the + * converter for the current conversion attempt + * @return the serialization view class, or {@code null} if none + */ + protected Class> getSerializationView(Object conversionHint) { + if (conversionHint instanceof MethodParameter) { + MethodParameter methodParam = (MethodParameter) conversionHint; + JsonView annotation = methodParam.getParameterAnnotation(JsonView.class); + if (annotation == null) { + annotation = methodParam.getMethodAnnotation(JsonView.class); + if (annotation == null) { + return null; + } + } + return extractViewClass(annotation, conversionHint); + } + else if (conversionHint instanceof JsonView) { + return extractViewClass((JsonView) conversionHint, conversionHint); + } + else if (conversionHint instanceof Class) { + return (Class) conversionHint; + } + else { + return null; + } + } + + private Class> extractViewClass(JsonView annotation, Object conversionHint) { + Class>[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for handler methods with exactly 1 class argument: " + conversionHint); + } + return classes[0]; + } + } 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 6b793fa363..1c3b88a6e0 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 @@ -24,6 +24,7 @@ import org.springframework.jms.support.JmsHeaderMapper; import org.springframework.jms.support.SimpleJmsHeaderMapper; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.core.AbstractMessagingTemplate; import org.springframework.util.Assert; /** @@ -92,8 +93,11 @@ public class MessagingMessageConverter implements MessageConverter, Initializing Message.class.getName() + "] is handled by this converter"); } Message> input = (Message>) object; - javax.jms.Message reply = createMessageForPayload(input.getPayload(), session); - this.headerMapper.fromHeaders(input.getHeaders(), reply); + MessageHeaders headers = input.getHeaders(); + Object conversionHint = (headers != null ? headers.get( + AbstractMessagingTemplate.CONVERSION_HINT_HEADER) : null); + javax.jms.Message reply = createMessageForPayload(input.getPayload(), session, conversionHint); + this.headerMapper.fromHeaders(headers, reply); return reply; } @@ -116,11 +120,26 @@ public class MessagingMessageConverter implements MessageConverter, Initializing /** * Create a JMS message for the specified payload. * @see MessageConverter#toMessage(Object, Session) + * @deprecated as of 4.3, use {@link #createMessageForPayload(Object, Session, Object)} */ + @Deprecated protected javax.jms.Message createMessageForPayload(Object payload, Session session) throws JMSException { return this.payloadConverter.toMessage(payload, session); } + /** + * Create a JMS message for the specified payload and conversionHint. The conversion + * hint is an extra object passed to the {@link MessageConverter}, e.g. the associated + * {@code MethodParameter} (may be {@code null}}. + * @see MessageConverter#toMessage(Object, Session) + * @since 4.3 + */ + @SuppressWarnings("deprecation") + protected javax.jms.Message createMessageForPayload(Object payload, Session session, Object conversionHint) + throws JMSException { + return createMessageForPayload(payload, session); + } + private MessageHeaders extractHeaders(javax.jms.Message message) { return this.headerMapper.toHeaders(message); } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java new file mode 100644 index 0000000000..2fb5cd734e --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SmartMessageConverter.java @@ -0,0 +1,51 @@ +/* + * 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. + * 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.support.converter; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +/** + * An extended {@link MessageConverter} SPI with conversion hint support. + * + *
In case of a conversion hint being provided, the framework will call
+ * the extended method if a converter implements this interface, instead
+ * of calling the regular {@code toMessage} variant.
+ *
+ * @author Stephane Nicoll
+ * @since 4.3
+ */
+public interface SmartMessageConverter extends MessageConverter {
+
+ /**
+ * A variant of {@link #toMessage(Object, Session)} which takes an extra conversion
+ * context as an argument, allowing to take e.g. annotations on a payload parameter
+ * into account.
+ * @param object the object to convert
+ * @param session the Session to use for creating a JMS Message
+ * @param conversionHint an extra object passed to the {@link MessageConverter},
+ * e.g. the associated {@code MethodParameter} (may be {@code null}}
+ * @return the JMS Message
+ * @throws javax.jms.JMSException if thrown by JMS API methods
+ * @throws MessageConversionException in case of conversion failure
+ * @see #toMessage(Object, Session)
+ */
+ Message toMessage(Object object, Session session, Object conversionHint)
+ throws JMSException, MessageConversionException;
+
+}
diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java
index 9f87143aa5..0b574b36f3 100644
--- a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java
+++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java
@@ -27,6 +27,7 @@ import javax.jms.Session;
import javax.jms.TextMessage;
import javax.jms.Topic;
+import com.fasterxml.jackson.annotation.JsonView;
import org.junit.Before;
import org.junit.Test;
@@ -236,7 +237,21 @@ public class MessagingMessageListenerAdapterTests {
verify(reply).setObjectProperty("foo", "bar");
}
- private TextMessage testReplyWithJackson(String methodName, String replyContent) throws JMSException {
+ @Test
+ public void replyJacksonMessageAndJsonView() throws JMSException {
+ TextMessage reply = testReplyWithJackson("replyJacksonMessageAndJsonView",
+ "{\"name\":\"Response\"}");
+ verify(reply).setObjectProperty("foo", "bar");
+ }
+
+ @Test
+ public void replyJacksonPojoAndJsonView() throws JMSException {
+ TextMessage reply = testReplyWithJackson("replyJacksonPojoAndJsonView",
+ "{\"name\":\"Response\"}");
+ verify(reply, never()).setObjectProperty("foo", "bar");
+ }
+
+ public TextMessage testReplyWithJackson(String methodName, String replyContent) throws JMSException {
Queue replyDestination = mock(Queue.class);
Session session = mock(Session.class);
@@ -327,6 +342,17 @@ public class MessagingMessageListenerAdapterTests {
.setHeader("foo", "bar").build();
}
+ @JsonView(Summary.class)
+ public Message