GH-1220: Add replyContentType to @RabbitListener

Resolves https://github.com/spring-projects/spring-amqp/issues/1220

Fix Typo
This commit is contained in:
Gary Russell
2020-07-14 12:29:27 -04:00
committed by GitHub
parent 91bd658f66
commit e0ca772304
11 changed files with 279 additions and 9 deletions

View File

@@ -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");

View File

@@ -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";
}

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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) {
...

View File

@@ -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.