GH-1219: Fix header mapping for replies (@SendTo)
Resolves https://github.com/spring-projects/spring-amqp/issues/1219 The headers were mapped after message conversion. This prevented using a `ContentTypeDelegatingMessageConverter` because the content type was not set. Add a header to control whether the user or converter gets to set the content type property in the final message. **cherry-pick to 2.2.x, 2.1.x, 1.7.x** # Conflicts: # spring-amqp/src/main/java/org/springframework/amqp/support/AmqpHeaders.java # spring-amqp/src/main/java/org/springframework/amqp/support/converter/MessagingMessageConverter.java # src/reference/asciidoc/amqp.adoc # Conflicts: # src/reference/asciidoc/amqp.adoc
This commit is contained in:
committed by
Artem Bilan
parent
c1566ffb6c
commit
16a87df36b
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-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.
|
||||
@@ -49,6 +49,8 @@ public abstract class AmqpHeaders {
|
||||
|
||||
public static final String CONTENT_TYPE = MessageHeaders.CONTENT_TYPE;
|
||||
|
||||
public static final String CONTENT_TYPE_CONVERTER_WINS = PREFIX + "contentTypeConverterWins";
|
||||
|
||||
public static final String CORRELATION_ID = PREFIX + "correlationId";
|
||||
|
||||
public static final String DELAY = PREFIX + "delay";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014 the original author or authors.
|
||||
* Copyright 2014-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.
|
||||
@@ -20,9 +20,11 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.support.AmqpHeaderMapper;
|
||||
import org.springframework.amqp.support.AmqpHeaders;
|
||||
import org.springframework.amqp.support.SimpleAmqpHeaderMapper;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -101,10 +103,16 @@ public class MessagingMessageConverter implements MessageConverter, Initializing
|
||||
Message.class.getName() + "] is handled by this converter");
|
||||
}
|
||||
Message<?> input = (Message<?>) object;
|
||||
this.headerMapper.fromHeaders(input.getHeaders(), messageProperties);
|
||||
org.springframework.amqp.core.Message amqpMessage = this.payloadConverter.toMessage(
|
||||
input.getPayload(), messageProperties);
|
||||
|
||||
this.headerMapper.fromHeaders(input.getHeaders(), messageProperties);
|
||||
// Default previous behavior of mapper wins for backwards compatibility.
|
||||
if (!Boolean.TRUE.equals(input.getHeaders().get(AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS))) {
|
||||
String contentType = input.getHeaders().get(MessageHeaders.CONTENT_TYPE, String.class);
|
||||
if (contentType != null) {
|
||||
messageProperties.setContentType(contentType);
|
||||
}
|
||||
}
|
||||
return amqpMessage;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Copyright 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://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.amqp.rabbit.annotation;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.amqp.core.AnonymousQueue;
|
||||
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.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitAdmin;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.junit.BrokerRunning;
|
||||
import org.springframework.amqp.support.AmqpHeaders;
|
||||
import org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter;
|
||||
import org.springframework.amqp.support.converter.MessageConversionException;
|
||||
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;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.handler.annotation.SendTo;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 1.7.15
|
||||
*
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
public class ContentTypeDelegatingMessageConverterIntegrationTests {
|
||||
|
||||
@ClassRule
|
||||
public static final BrokerRunning BROKER_RUNNING = BrokerRunning.isRunning();
|
||||
|
||||
@Autowired
|
||||
private Config config;
|
||||
|
||||
@Autowired
|
||||
private RabbitTemplate template;
|
||||
|
||||
@Autowired
|
||||
private AnonymousQueue queue1;
|
||||
|
||||
@Test
|
||||
public void testReplyContentType() throws InterruptedException {
|
||||
this.template.convertAndSend(this.queue1.getName(), "foo", msg -> {
|
||||
msg.getMessageProperties().setContentType("foo/bar");
|
||||
return msg;
|
||||
});
|
||||
assertTrue(this.config.latch1.await(10, TimeUnit.SECONDS));
|
||||
assertEquals("baz/qux", this.config.replyContentType);
|
||||
assertEquals("baz/qux", this.config.receivedReplyContentType);
|
||||
|
||||
this.template.convertAndSend(this.queue1.getName(), "bar", msg -> {
|
||||
msg.getMessageProperties().setContentType("foo/bar");
|
||||
return msg;
|
||||
});
|
||||
assertTrue(this.config.latch2.await(10, TimeUnit.SECONDS));
|
||||
assertEquals("baz/qux", this.config.replyContentType);
|
||||
assertEquals("foo/bar", this.config.receivedReplyContentType);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRabbit
|
||||
public static class Config {
|
||||
|
||||
final CountDownLatch latch1 = new CountDownLatch(1);
|
||||
|
||||
final CountDownLatch latch2 = new CountDownLatch(2);
|
||||
|
||||
volatile String replyContentType;
|
||||
|
||||
volatile String receivedReplyContentType;
|
||||
|
||||
@Bean
|
||||
public ConnectionFactory cf() {
|
||||
return new CachingConnectionFactory(BROKER_RUNNING.getConnectionFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
|
||||
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
|
||||
factory.setConnectionFactory(cf());
|
||||
ContentTypeDelegatingMessageConverter converter =
|
||||
new ContentTypeDelegatingMessageConverter(new SimpleMessageConverter());
|
||||
converter.addDelegate("foo/bar", new MessageConverter() {
|
||||
|
||||
@Override
|
||||
public Message toMessage(Object object, MessageProperties messageProperties)
|
||||
throws MessageConversionException {
|
||||
|
||||
return new Message("foo".getBytes(), messageProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(Message message) throws MessageConversionException {
|
||||
return new String(message.getBody());
|
||||
}
|
||||
|
||||
});
|
||||
converter.addDelegate("baz/qux", new MessageConverter() {
|
||||
|
||||
@Override
|
||||
public Message toMessage(Object object, MessageProperties messageProperties)
|
||||
throws MessageConversionException {
|
||||
|
||||
Config.this.replyContentType = messageProperties.getContentType();
|
||||
messageProperties.setContentType("foo/bar");
|
||||
return new Message("foo".getBytes(), messageProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object fromMessage(Message message) throws MessageConversionException {
|
||||
return new String(message.getBody());
|
||||
}
|
||||
|
||||
});
|
||||
factory.setMessageConverter(converter);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RabbitTemplate template() {
|
||||
return new RabbitTemplate(cf());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RabbitAdmin admin() {
|
||||
return new RabbitAdmin(cf());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AnonymousQueue queue1() {
|
||||
return new AnonymousQueue();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AnonymousQueue queue2() {
|
||||
return new AnonymousQueue();
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{@queue1.name}")
|
||||
@SendTo("#{@queue2.name}")
|
||||
public org.springframework.messaging.Message<String> listen1(String in) {
|
||||
MessageBuilder<String> builder = MessageBuilder.withPayload(in)
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, "baz/qux");
|
||||
if ("bar".equals(in)) {
|
||||
builder.setHeader(AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS, true);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{@queue2.name}")
|
||||
public void listen2(Message in) {
|
||||
this.receivedReplyContentType = in.getMessageProperties().getContentType();
|
||||
this.latch1.countDown();
|
||||
this.latch2.countDown();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1896,7 +1896,7 @@ public Message<OrderStatus> processOrder(Order order) {
|
||||
}
|
||||
----
|
||||
|
||||
The `@SendTo` value is assumed as a reply `exchange` and `routingKey` pair following the pattern `exchange/routingKey`,
|
||||
The `@SendTo` value is assumed as a reply `exchange` and `routingKey` pair that follows the `exchange/routingKey` pattern,
|
||||
where one of those parts can be omitted.
|
||||
The valid values are:
|
||||
|
||||
@@ -1968,6 +1968,28 @@ beans are referenced by their names.
|
||||
`!{...}` is evaluated at runtime for each message with the root object having the properties above and beans are
|
||||
referenced with their names, prefixed by `@`.
|
||||
|
||||
====== 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<?>`:
|
||||
|
||||
====
|
||||
[source, java]
|
||||
----
|
||||
@RabbitListener(queues = "q1")
|
||||
@SendTo("q2")
|
||||
public Message<String> listen(String in) {
|
||||
...
|
||||
return MessageBuilder.withPayload(in.toUpperCase())
|
||||
.setHeader(MessageHeaders.CONTENT_TYPE, "application/xml")
|
||||
.build();
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This content type will be passed in the `MessageProperties` to the converter.
|
||||
By default, for backwards compatibility, any content type property set by the converter will be overwritten by this value after conversion.
|
||||
If you wish to override that behavior, also set the `AmqpHeaders.CONTENT_TYPE_CONVERTER_WINS` to `true` and any value set by the converter will be retained.
|
||||
|
||||
[[annotation-method-selection]]
|
||||
====== Multi-Method Listeners
|
||||
|
||||
|
||||
Reference in New Issue
Block a user