diff --git a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java index fe03b837..b8c97a52 100644 --- a/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java +++ b/spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java @@ -101,7 +101,9 @@ public class MessagingMessageConverter implements MessageConverter, Initializing } @Override - public org.springframework.amqp.core.Message toMessage(Object object, MessageProperties messageProperties) throws MessageConversionException { + public org.springframework.amqp.core.Message toMessage(Object object, MessageProperties messageProperties) + throws MessageConversionException { + if (!(object instanceof Message)) { throw new IllegalArgumentException("Could not convert [" + object + "] - only [" + Message.class.getName() + "] is handled by this converter"); diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java index 647f899d..08287d4a 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java @@ -273,4 +273,46 @@ public @interface RabbitListener { */ String replyPostProcessor() default ""; + /** + * Override the container factory's message converter used for this listener. + * @return the message converter bean name. + * @since 2.3 + */ + String messageConverter() default ""; + + /** + * Used to set the content type of a reply message. Useful when used in conjunction + * with message converters that can handle multiple content types, such as the + * {@link org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter}. + * SpEL expressions and property placeholders are supported. Also useful if you wish to + * control the final content type property when used with certain converters. This does + * not apply when the return type is {@link org.springframework.amqp.core.Message} or + * {@link org.springframework.messaging.Message}; set the content type message property + * or header respectively, in those cases. + * @return the content type. + * @since 2.3 + * @see #converterWinsContentType() + */ + String replyContentType() default ""; + + /** + * Set to 'false' to override any content type headers set by the message converter + * with the value of the 'replyContentType' property. Some converters, such as the + * {@link org.springframework.amqp.support.converter.SimpleMessageConverter} use the + * payload type and set the content type header appropriately. For example, if you set + * the 'replyContentType' to "application/json" and use the simple message converter + * when returning a String containing JSON, the converter will overwrite the content + * type to 'text/plain'. Set this to false, to prevent that action. This does not + * apply when the return type is {@link org.springframework.amqp.core.Message} because + * there is no conversion involved. When returning a + * {@link org.springframework.messaging.Message}, set the content type message header + * and + * {@link org.springframework.amqp.support.AmqpHeaders#CONTENT_TYPE_CONVERTER_WINS} to + * false. + * @return false to use the replyContentType. + * @since 2.3 + * @see #replyContentType() + */ + String converterWinsContentType() default "true"; + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java index 17d82b64..95532ad8 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java @@ -54,6 +54,7 @@ import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar; import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor; import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler; +import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeansException; @@ -456,6 +457,8 @@ public class RabbitListenerAnnotationBeanPostProcessor resolveAdmin(endpoint, rabbitListener, target); resolveAckMode(endpoint, rabbitListener); resolvePostProcessor(endpoint, rabbitListener, target, beanName); + resolveMessageConverter(endpoint, rabbitListener, target, beanName); + resolveReplyContentType(endpoint, rabbitListener); RabbitListenerContainerFactory factory = resolveContainerFactory(rabbitListener, target, beanName); this.registrar.registerEndpoint(endpoint, factory); @@ -546,6 +549,30 @@ public class RabbitListenerAnnotationBeanPostProcessor } } + private void resolveMessageConverter(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener, + Object target, String beanName) { + + String mcBeanName = resolveExpressionAsString(rabbitListener.messageConverter(), "messageConverter"); + if (StringUtils.hasText(mcBeanName)) { + assertBeanFactory(); + try { + endpoint.setMessageConverter(this.beanFactory.getBean(mcBeanName, MessageConverter.class)); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanInitializationException( + noBeanFoundMessage(target, beanName, mcBeanName, MessageConverter.class), ex); + } + } + } + + private void resolveReplyContentType(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener) { + String contentType = resolveExpressionAsString(rabbitListener.replyContentType(), "replyContentType"); + if (StringUtils.hasText(contentType)) { + endpoint.setReplyContentType(contentType); + endpoint.setConverterWinsContentType(resolveExpressionAsBoolean(rabbitListener.converterWinsContentType())); + } + } + protected void assertBeanFactory() { Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name"); } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java index 3eee538b..accedb0d 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java @@ -395,7 +395,7 @@ public abstract class AbstractRabbitListenerContainerFactory diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java index 623e5d76..15dfc40c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/MessagingMessageListenerAdapter.java @@ -252,7 +252,7 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis return this.messagingMessageConverter.toMessage(result, new MessageProperties()); } else { - return converter.toMessage(result, new MessageProperties(), genericType); + return convert(result, genericType, converter); } } else { diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java index 4d82dd89..b822bdc6 100644 --- a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitReturnTypesTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 the original author or authors. + * Copyright 2019-2020 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,12 +23,17 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.junit.RabbitAvailable; import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -43,7 +48,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @SpringJUnitConfig @DirtiesContext @RabbitAvailable(queues = { "EnableRabbitReturnTypesTests.1", "EnableRabbitReturnTypesTests.2", - "EnableRabbitReturnTypesTests.3", "EnableRabbitReturnTypesTests.4" }) + "EnableRabbitReturnTypesTests.3", "EnableRabbitReturnTypesTests.4", "EnableRabbitReturnTypesTests.5" }) public class EnableRabbitReturnTypesTests { @Test @@ -77,6 +82,14 @@ public class EnableRabbitReturnTypesTests { assertThat(reply).isInstanceOf(Four.class); } + @Test + void testReturnContentType(@Autowired RabbitTemplate template) { + Message reply = template.sendAndReceive("EnableRabbitReturnTypesTests.5", + new Message("foo".getBytes(), new MessageProperties())); + assertThat(reply.getBody()).isEqualTo("FOO".getBytes()); + assertThat(reply.getMessageProperties().getContentType()).isEqualTo("foo/bar"); + } + @Configuration(proxyBeanMethods = false) @EnableRabbit public static class Config { @@ -147,6 +160,21 @@ public class EnableRabbitReturnTypesTests { } } + @RabbitListener(queues = "EnableRabbitReturnTypesTests.5", messageConverter = "delegating", + replyContentType = "foo/bar", converterWinsContentType = "false") + public String listen5(String in) { + return in.toUpperCase(); + } + + @Bean + public MessageConverter delegating() { + ContentTypeDelegatingMessageConverter converter = new ContentTypeDelegatingMessageConverter(); + SimpleMessageConverter messageConverter = new SimpleMessageConverter(); + converter.addDelegate("foo/bar", messageConverter); + converter.addDelegate("text/plain", messageConverter); + return converter; + } + } public interface One { diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index fab732df..ec8f3918 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -2436,6 +2436,25 @@ This configures a Jackson2 converter that expects header information to be prese You can also use a `ContentTypeDelegatingMessageConverter`, which can handle conversion of different content types. +Starting with version 2.3, you can override the factory converter by specifying a bean name in the `messageConverter` property. + +==== +[source, java] +---- +@Bean +public MessageConverter jsonConverter() { + return new Jackson2JsonMessageConverter(); +} + +@RabbitListener(..., messageConverter = "jsonConverter") +public void listen(String in) { + ... +} +---- +==== + +This avoids having to declare a different container factory just to change the converter. + In most cases, it is not necessary to customize the method argument converter unless, for example, you want to use a custom `ConversionService`. @@ -2800,14 +2819,49 @@ public String listen(Message in) { ---- ==== +[[reply-content-type]] ====== Reply ContentType -If you are using a sophisticated message converter, such as the `ContentTypeDelegatingMessageConverter`, you can control the content type of the reply by returning a `spring-messaging` `Message`: +If you are using a sophisticated message converter, such as the `ContentTypeDelegatingMessageConverter`, you can control the content type of the reply by setting the `replyContentType` property on the listener. +This allows the converter to select the appropriate delegate converter for the reply. ==== [source, java] ---- -@RabbitListener(queues = "q1") +@RabbitListener(queues = "q1", messageConverter = "delegating", + replyContentType = "application/json") +public Thing2 listen(Thing1 in) { + ... +} +---- +==== + +By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion. +Converters such as the `SimpleMessageConverter` use the reply type rather than the content type to determine the conversion needed and sets the content type in the reply message appropriately. +This may not be the desired action and can be overridden by setting the `converterWinsContentType` property to `false`. +For example, if you return a `String` containing JSON, the `SimpleMessageConverter` will set the content type in the reply to `text/plain`. +The following configuration will ensure the content type is set properly, even if the `SimpleMessageConverter` is used. + +==== +[source, java] +---- +@RabbitListener(queues = "q1", replyContentType = "application/json", + converterWinsContentType = "false") +public String listen(Thing in) { + ... + return someJsonString; +} +---- +==== + +These properties (`replyContentType` and `converterWinsContentType`) do not apply when the return type is a Spring AMQP `Message` or a Spring Messaging `Message`. +In the first case, there is no conversion involved; simply set the `contentType` message property. +In the second case, the behavior is controlled using message headers: + +==== +[source, java] +---- +@RabbitListener(queues = "q1", messageConverter = "delegating") @SendTo("q2") public Message listen(String in) { ... diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 4d5b3d23..c8eedef5 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -11,6 +11,11 @@ See <> for changes in previous versions. Two additional connection factories are now provided. See <> for more information. +==== `@RabbitListener` Changes + +You can now specify a reply content type. +See <> for more information. + ==== Message Converter Changes The `Jackson2JMessageConverter` s can now deserialize abstract classes (including interfaces) if the `ObjectMapper` is configured with a custom deserializer.