GH-1220: Add replyContentType to @RabbitListener
Resolves https://github.com/spring-projects/spring-amqp/issues/1220 Fix Typo
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ public abstract class AbstractRabbitListenerContainerFactory<C extends AbstractM
|
||||
JavaUtils.INSTANCE
|
||||
.acceptIfNotNull(this.connectionFactory, instance::setConnectionFactory)
|
||||
.acceptIfNotNull(this.errorHandler, instance::setErrorHandler);
|
||||
if (this.messageConverter != null && endpoint != null) {
|
||||
if (this.messageConverter != null && endpoint != null && endpoint.getMessageConverter() == null) {
|
||||
endpoint.setMessageConverter(this.messageConverter);
|
||||
}
|
||||
javaUtils
|
||||
@@ -443,7 +443,9 @@ public abstract class AbstractRabbitListenerContainerFactory<C extends AbstractM
|
||||
.acceptIfCondition(this.retryTemplate != null && this.recoveryCallback != null,
|
||||
this.recoveryCallback, messageListener::setRecoveryCallback)
|
||||
.acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected)
|
||||
.acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor);
|
||||
.acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor)
|
||||
.acceptIfNotNull(endpoint.getReplyContentType(), messageListener::setReplyContentType);
|
||||
messageListener.setConverterWinsContentType(endpoint.isConverterWinsContentType());
|
||||
}
|
||||
initializeContainer(instance, endpoint);
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn
|
||||
|
||||
private ReplyPostProcessor replyPostProcessor;
|
||||
|
||||
private String replyContentType;
|
||||
|
||||
private boolean converterWinsContentType = true;
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
@@ -338,6 +342,34 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn
|
||||
this.replyPostProcessor = replyPostProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReplyContentType() {
|
||||
return this.replyContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reply content type.
|
||||
* @param replyContentType the content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setReplyContentType(String replyContentType) {
|
||||
this.replyContentType = replyContentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConverterWinsContentType() {
|
||||
return this.converterWinsContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the content type set by a converter prevails or not.
|
||||
* @param converterWinsContentType false to always apply the reply content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setConverterWinsContentType(boolean converterWinsContentType) {
|
||||
this.converterWinsContentType = converterWinsContentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupListenerContainer(MessageListenerContainer listenerContainer) {
|
||||
AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) listenerContainer;
|
||||
|
||||
@@ -148,4 +148,22 @@ public interface RabbitListenerEndpoint {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reply content type.
|
||||
* @return the content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
default String getReplyContentType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the content type set by a converter prevails or not.
|
||||
* @return false to always apply the reply content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
default boolean isConverterWinsContentType() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
|
||||
|
||||
private ReplyPostProcessor replyPostProcessor;
|
||||
|
||||
private String replyContentType;
|
||||
|
||||
private boolean converterWinsContentType = true;
|
||||
|
||||
/**
|
||||
* Set the routing key to use when sending response messages.
|
||||
* This will be applied in case of a request message that
|
||||
@@ -255,6 +259,42 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
|
||||
this.replyPostProcessor = replyPostProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reply content type.
|
||||
* @return the content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
protected String getReplyContentType() {
|
||||
return this.replyContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reply content type.
|
||||
* @param replyContentType the content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setReplyContentType(String replyContentType) {
|
||||
this.replyContentType = replyContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the content type set by a converter prevails or not.
|
||||
* @return false to always apply the reply content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
protected boolean isConverterWinsContentType() {
|
||||
return this.converterWinsContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the content type set by a converter prevails or not.
|
||||
* @param converterWinsContentType false to always apply the reply content type.
|
||||
* @since 2.3
|
||||
*/
|
||||
public void setConverterWinsContentType(boolean converterWinsContentType) {
|
||||
this.converterWinsContentType = converterWinsContentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the converter that will convert incoming Rabbit messages to listener method arguments, and objects
|
||||
* returned from listener methods back to Rabbit messages.
|
||||
@@ -452,7 +492,7 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
|
||||
protected Message buildMessage(Channel channel, Object result, Type genericType) {
|
||||
MessageConverter converter = getMessageConverter();
|
||||
if (converter != null && !(result instanceof Message)) {
|
||||
return converter.toMessage(result, new MessageProperties(), genericType);
|
||||
return convert(result, genericType, converter);
|
||||
}
|
||||
else {
|
||||
if (!(result instanceof Message)) {
|
||||
@@ -463,6 +503,26 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to a message, with reply content type based on settings.
|
||||
* @param result the result.
|
||||
* @param genericType the type.
|
||||
* @param converter the converter.
|
||||
* @return the message.
|
||||
* @since 2.3
|
||||
*/
|
||||
protected Message convert(Object result, Type genericType, MessageConverter converter) {
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
if (this.replyContentType != null) {
|
||||
messageProperties.setContentType(this.replyContentType);
|
||||
}
|
||||
Message message = converter.toMessage(result, messageProperties, genericType);
|
||||
if (this.replyContentType != null && !this.converterWinsContentType) {
|
||||
message.getMessageProperties().setContentType(this.replyContentType);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-process the given response message before it will be sent.
|
||||
* <p>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<O extends One> {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> listen(String in) {
|
||||
...
|
||||
|
||||
@@ -11,6 +11,11 @@ See <<change-history>> for changes in previous versions.
|
||||
Two additional connection factories are now provided.
|
||||
See <<choosing-factory>> for more information.
|
||||
|
||||
==== `@RabbitListener` Changes
|
||||
|
||||
You can now specify a reply content type.
|
||||
See <<reply-content-type>> 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.
|
||||
|
||||
Reference in New Issue
Block a user