Skip to content

Commit 54de7a2

Browse files
artembilangaryrussell
authored andcommitted
GH-3089: Add AmqpInGateway.replyHeadersMappedLast (#3091)
* GH-3089: Add AmqpInGateway.replyHeadersMappedLast Fixes #3089 In some use-case we would like to control when headers from SI message should be populated into an AMQP message. One of the use-case is like a `SimpleMessageConverter` and its `plain/text` for the String reply, meanwhile we know that this content is an `application/json`. So, with a new `replyHeadersMappedLast` we can override the mentioned `content-type` header, populated by the `MessageConverter` with an actual value from the message headers populated in the flow upstream * Introduce an `AmqpInboundGateway.replyHeadersMappedLast`; expose it on the DSL and XML level * Use newly introduced `MappingUtils.mapReplyMessage()` * Optimize `DefaultAmqpHeaderMapper` to not parse JSON headers at all when `JsonHeaders.TYPE_ID` is already present (e.g. `MessageConverter` result) * Also skip `JsonHeaders` when we `populateUserDefinedHeader()` **Cherry-pick to 5.1.x** * * Fix language and package typos * Add missed `@param` in JavaDoc of the `AmqpBaseInboundGatewaySpec.batchingStrategy()` * Extract a `RabbitTemplate` `MessageConverter` to use for reply messages conversion - pursue a backward compatibility
1 parent 315fafd commit 54de7a2

File tree

11 files changed

+249
-101
lines changed

11 files changed

+249
-101
lines changed

spring-integration-amqp/src/main/java/org/springframework/integration/amqp/config/AmqpInboundGatewayParser.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author Mark Fisher
3131
* @author Gary Russell
3232
* @author Artem Bilan
33+
*
3334
* @since 2.1
3435
*/
3536
public class AmqpInboundGatewayParser extends AbstractAmqpInboundAdapterParser {
@@ -48,6 +49,8 @@ protected void configureChannels(Element element, ParserContext parserContext, B
4849
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-channel");
4950
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
5051
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "default-reply-to");
52+
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-headers-last",
53+
"replyHeadersMappedLast");
5154
}
5255

5356
}

spring-integration-amqp/src/main/java/org/springframework/integration/amqp/dsl/AmqpBaseInboundGatewaySpec.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,29 @@ public S recoveryCallback(RecoveryCallback<?> recoveryCallback) {
138138
return _this();
139139
}
140140

141+
/**
142+
* Set to true to bind the source message in the headers.
143+
* @param bindSourceMessage true to bind.
144+
* @return the spec.
145+
* @since 5.1.9
146+
* @see AmqpInboundGateway#setBindSourceMessage(boolean)
147+
*/
148+
public S bindSourceMessage(boolean bindSourceMessage) {
149+
this.target.setBindSourceMessage(bindSourceMessage);
150+
return _this();
151+
}
152+
153+
/**
154+
* When mapping headers for the outbound (reply) message, determine whether the headers are
155+
* mapped before the message is converted, or afterwards.
156+
* @param replyHeadersMappedLast true if reply headers are mapped after conversion.
157+
* @return the spec.
158+
* @since 5.1.9
159+
* @see AmqpInboundGateway#setReplyHeadersMappedLast(boolean)
160+
*/
161+
public S replyHeadersMappedLast(boolean replyHeadersMappedLast) {
162+
this.target.setReplyHeadersMappedLast(replyHeadersMappedLast);
163+
return _this();
164+
}
165+
141166
}

spring-integration-amqp/src/main/java/org/springframework/integration/amqp/inbound/AmqpInboundGateway.java

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import org.springframework.amqp.core.Address;
2424
import org.springframework.amqp.core.AmqpTemplate;
2525
import org.springframework.amqp.core.Message;
26-
import org.springframework.amqp.core.MessagePostProcessor;
27-
import org.springframework.amqp.core.MessageProperties;
2826
import org.springframework.amqp.rabbit.core.RabbitTemplate;
2927
import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer;
3028
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
@@ -38,14 +36,14 @@
3836
import org.springframework.integration.amqp.support.AmqpMessageHeaderErrorMessageStrategy;
3937
import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper;
4038
import org.springframework.integration.amqp.support.EndpointUtils;
39+
import org.springframework.integration.amqp.support.MappingUtils;
4140
import org.springframework.integration.gateway.MessagingGatewaySupport;
4241
import org.springframework.integration.support.ErrorMessageUtils;
4342
import org.springframework.messaging.MessageChannel;
4443
import org.springframework.retry.RecoveryCallback;
4544
import org.springframework.retry.support.RetrySynchronizationManager;
4645
import org.springframework.retry.support.RetryTemplate;
4746
import org.springframework.util.Assert;
48-
import org.springframework.util.StringUtils;
4947

5048
import com.rabbitmq.client.Channel;
5149

@@ -71,9 +69,11 @@ public class AmqpInboundGateway extends MessagingGatewaySupport {
7169

7270
private final boolean amqpTemplateExplicitlySet;
7371

74-
private volatile MessageConverter amqpMessageConverter = new SimpleMessageConverter();
72+
private MessageConverter amqpMessageConverter = new SimpleMessageConverter();
7573

76-
private volatile AmqpHeaderMapper headerMapper = DefaultAmqpHeaderMapper.inboundMapper();
74+
private MessageConverter templateMessageConverter = this.amqpMessageConverter;
75+
76+
private AmqpHeaderMapper headerMapper = DefaultAmqpHeaderMapper.inboundMapper();
7777

7878
private Address defaultReplyTo;
7979

@@ -83,6 +83,8 @@ public class AmqpInboundGateway extends MessagingGatewaySupport {
8383

8484
private boolean bindSourceMessage;
8585

86+
private boolean replyHeadersMappedLast;
87+
8688
public AmqpInboundGateway(AbstractMessageListenerContainer listenerContainer) {
8789
this(listenerContainer, new RabbitTemplate(listenerContainer.getConnectionFactory()), false);
8890
}
@@ -110,6 +112,9 @@ private AmqpInboundGateway(AbstractMessageListenerContainer listenerContainer, A
110112
this.messageListenerContainer.setAutoStartup(false);
111113
this.amqpTemplate = amqpTemplate;
112114
this.amqpTemplateExplicitlySet = amqpTemplateExplicitlySet;
115+
if (this.amqpTemplateExplicitlySet && this.amqpTemplate instanceof RabbitTemplate) {
116+
this.templateMessageConverter = ((RabbitTemplate) this.amqpTemplate).getMessageConverter();
117+
}
113118
setErrorMessageStrategy(new AmqpMessageHeaderErrorMessageStrategy());
114119
}
115120

@@ -125,6 +130,7 @@ public void setMessageConverter(MessageConverter messageConverter) {
125130
this.amqpMessageConverter = messageConverter;
126131
if (!this.amqpTemplateExplicitlySet) {
127132
((RabbitTemplate) this.amqpTemplate).setMessageConverter(messageConverter);
133+
this.templateMessageConverter = messageConverter;
128134
}
129135
}
130136

@@ -187,6 +193,24 @@ public void setBindSourceMessage(boolean bindSourceMessage) {
187193
this.bindSourceMessage = bindSourceMessage;
188194
}
189195

196+
/**
197+
* When mapping headers for the outbound (reply) message, determine whether the headers are
198+
* mapped before the message is converted, or afterwards. This only affects headers
199+
* that might be added by the message converter. When false, the converter's headers
200+
* win; when true, any headers added by the converter will be overridden (if the
201+
* source message has a header that maps to those headers). You might wish to set this
202+
* to true, for example, when using a
203+
* {@link org.springframework.amqp.support.converter.SimpleMessageConverter} with a
204+
* String payload that contains json; the converter will set the content type to
205+
* {@code text/plain} which can be overridden to {@code application/json} by setting
206+
* the {@link AmqpHeaders#CONTENT_TYPE} message header. Default: false.
207+
* @param replyHeadersMappedLast true if reply headers are mapped after conversion.
208+
* @since 5.1.9
209+
*/
210+
public void setReplyHeadersMappedLast(boolean replyHeadersMappedLast) {
211+
this.replyHeadersMappedLast = replyHeadersMappedLast;
212+
}
213+
190214
@Override
191215
public String getComponentType() {
192216
return "amqp:inbound-gateway";
@@ -331,7 +355,7 @@ private org.springframework.messaging.Message<Object> convert(Message message, C
331355

332356
private void process(Message message, org.springframework.messaging.Message<Object> messagingMessage) {
333357
setAttributesIfNecessary(message, messagingMessage);
334-
final org.springframework.messaging.Message<?> reply = sendAndReceiveMessage(messagingMessage);
358+
org.springframework.messaging.Message<?> reply = sendAndReceiveMessage(messagingMessage);
335359
if (reply != null) {
336360
Address replyTo;
337361
String replyToProperty = message.getMessageProperties().getReplyTo();
@@ -342,39 +366,23 @@ private void process(Message message, org.springframework.messaging.Message<Obje
342366
replyTo = AmqpInboundGateway.this.defaultReplyTo;
343367
}
344368

345-
MessagePostProcessor messagePostProcessor =
346-
message1 -> {
347-
MessageProperties messageProperties = message1.getMessageProperties();
348-
String contentEncoding = messageProperties.getContentEncoding();
349-
long contentLength = messageProperties.getContentLength();
350-
String contentType = messageProperties.getContentType();
351-
AmqpInboundGateway.this.headerMapper.fromHeadersToReply(reply.getHeaders(),
352-
messageProperties);
353-
// clear the replyTo from the original message since we are using it now
354-
messageProperties.setReplyTo(null);
355-
// reset the content-* properties as determined by the MessageConverter
356-
if (StringUtils.hasText(contentEncoding)) {
357-
messageProperties.setContentEncoding(contentEncoding);
358-
}
359-
messageProperties.setContentLength(contentLength);
360-
if (contentType != null) {
361-
messageProperties.setContentType(contentType);
362-
}
363-
return message1;
364-
};
369+
org.springframework.amqp.core.Message amqpMessage =
370+
MappingUtils.mapReplyMessage(reply, AmqpInboundGateway.this.templateMessageConverter,
371+
AmqpInboundGateway.this.headerMapper,
372+
message.getMessageProperties().getReceivedDeliveryMode(),
373+
AmqpInboundGateway.this.replyHeadersMappedLast);
365374

366375
if (replyTo != null) {
367-
AmqpInboundGateway.this.amqpTemplate.convertAndSend(replyTo.getExchangeName(),
368-
replyTo.getRoutingKey(), reply.getPayload(), messagePostProcessor);
376+
AmqpInboundGateway.this.amqpTemplate.send(replyTo.getExchangeName(), replyTo.getRoutingKey(),
377+
amqpMessage);
369378
}
370379
else {
371380
if (!AmqpInboundGateway.this.amqpTemplateExplicitlySet) {
372381
throw new IllegalStateException("There is no 'replyTo' message property " +
373382
"and the `defaultReplyTo` hasn't been configured.");
374383
}
375384
else {
376-
AmqpInboundGateway.this.amqpTemplate.convertAndSend(reply.getPayload(),
377-
messagePostProcessor);
385+
AmqpInboundGateway.this.amqpTemplate.send(amqpMessage);
378386
}
379387
}
380388
}

spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ protected DefaultAmqpHeaderMapper(String[] requestHeaderNames, String[] replyHea
105105
*/
106106
@Override
107107
protected Map<String, Object> extractStandardHeaders(MessageProperties amqpMessageProperties) {
108-
Map<String, Object> headers = new HashMap<String, Object>();
108+
Map<String, Object> headers = new HashMap<>();
109109
try {
110110
String appId = amqpMessageProperties.getAppId();
111111
if (StringUtils.hasText(appId)) {
@@ -325,24 +325,23 @@ else if (allHeaders != null) {
325325
amqpMessageProperties.setUserId(userId);
326326
}
327327

328-
Map<String, String> jsonHeaders = new HashMap<String, String>();
329-
330-
for (String jsonHeader : JsonHeaders.HEADERS) {
331-
Object value = getHeaderIfAvailable(headers, jsonHeader, Object.class);
332-
if (value != null) {
333-
headers.remove(jsonHeader);
334-
if (value instanceof Class<?>) {
335-
value = ((Class<?>) value).getName();
336-
}
337-
jsonHeaders.put(jsonHeader.replaceFirst(JsonHeaders.PREFIX, ""), value.toString());
338-
}
339-
}
340-
341328
/*
342329
* If the MessageProperties already contains JsonHeaders, don't overwrite them here because they were
343330
* set up by a message converter.
344331
*/
345332
if (!amqpMessageProperties.getHeaders().containsKey(JsonHeaders.TYPE_ID.replaceFirst(JsonHeaders.PREFIX, ""))) {
333+
Map<String, String> jsonHeaders = new HashMap<>();
334+
335+
for (String jsonHeader : JsonHeaders.HEADERS) {
336+
Object value = getHeaderIfAvailable(headers, jsonHeader, Object.class);
337+
if (value != null) {
338+
headers.remove(jsonHeader);
339+
if (value instanceof Class<?>) {
340+
value = ((Class<?>) value).getName();
341+
}
342+
jsonHeaders.put(jsonHeader.replaceFirst(JsonHeaders.PREFIX, ""), value.toString());
343+
}
344+
}
346345
amqpMessageProperties.getHeaders().putAll(jsonHeaders);
347346
}
348347

@@ -361,8 +360,9 @@ protected void populateUserDefinedHeader(String headerName, Object headerValue,
361360
MessageProperties amqpMessageProperties) {
362361
// do not overwrite an existing header with the same key
363362
// TODO: do we need to expose a boolean 'overwrite' flag?
364-
if (!amqpMessageProperties.getHeaders().containsKey(headerName)
365-
&& !AmqpHeaders.CONTENT_TYPE.equals(headerName)) {
363+
if (!amqpMessageProperties.getHeaders().containsKey(headerName) &&
364+
!AmqpHeaders.CONTENT_TYPE.equals(headerName) &&
365+
!headerName.startsWith(JsonHeaders.PREFIX)) {
366366
amqpMessageProperties.setHeader(headerName, headerValue);
367367
}
368368
}

spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/MappingUtils.java

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
* Utility methods used during message mapping.
3131
*
3232
* @author Gary Russell
33+
* @author Artem Bilan
34+
*
3335
* @since 4.3
3436
*
3537
*/
@@ -40,7 +42,7 @@ private MappingUtils() {
4042
}
4143

4244
/**
43-
* Map an o.s.Message to an o.s.a.core.Message. When using a
45+
* Map an o.s.m.Message to an o.s.a.core.Message. When using a
4446
* {@link ContentTypeDelegatingMessageConverter}, {@link AmqpHeaders#CONTENT_TYPE} and
4547
* {@link MessageHeaders#CONTENT_TYPE} will be used for the selection, with the AMQP
4648
* header taking precedence.
@@ -54,25 +56,64 @@ private MappingUtils() {
5456
public static org.springframework.amqp.core.Message mapMessage(Message<?> requestMessage,
5557
MessageConverter converter, AmqpHeaderMapper headerMapper, MessageDeliveryMode defaultDeliveryMode,
5658
boolean headersMappedLast) {
59+
60+
return doMapMessage(requestMessage, converter, headerMapper, defaultDeliveryMode, headersMappedLast, false);
61+
}
62+
63+
/**
64+
* Map a reply o.s.m.Message to an o.s.a.core.Message. When using a
65+
* {@link ContentTypeDelegatingMessageConverter}, {@link AmqpHeaders#CONTENT_TYPE} and
66+
* {@link MessageHeaders#CONTENT_TYPE} will be used for the selection, with the AMQP
67+
* header taking precedence.
68+
* @param replyMessage the reply message.
69+
* @param converter the message converter to use.
70+
* @param headerMapper the header mapper to use.
71+
* @param defaultDeliveryMode the default delivery mode.
72+
* @param headersMappedLast true if headers are mapped after conversion.
73+
* @return the mapped Message.
74+
* @since 5.1.9
75+
*/
76+
public static org.springframework.amqp.core.Message mapReplyMessage(Message<?> replyMessage,
77+
MessageConverter converter, AmqpHeaderMapper headerMapper, MessageDeliveryMode defaultDeliveryMode,
78+
boolean headersMappedLast) {
79+
80+
return doMapMessage(replyMessage, converter, headerMapper, defaultDeliveryMode, headersMappedLast, true);
81+
}
82+
83+
private static org.springframework.amqp.core.Message doMapMessage(Message<?> message,
84+
MessageConverter converter, AmqpHeaderMapper headerMapper, MessageDeliveryMode defaultDeliveryMode,
85+
boolean headersMappedLast, boolean reply) {
86+
5787
MessageProperties amqpMessageProperties = new MessageProperties();
5888
org.springframework.amqp.core.Message amqpMessage;
5989
if (!headersMappedLast) {
60-
headerMapper.fromHeadersToRequest(requestMessage.getHeaders(), amqpMessageProperties);
90+
mapHeaders(message.getHeaders(), amqpMessageProperties, headerMapper, reply);
6191
}
6292
if (converter instanceof ContentTypeDelegatingMessageConverter && headersMappedLast) {
63-
String contentType = contentTypeAsString(requestMessage.getHeaders());
93+
String contentType = contentTypeAsString(message.getHeaders());
6494
if (contentType != null) {
6595
amqpMessageProperties.setContentType(contentType);
6696
}
6797
}
68-
amqpMessage = converter.toMessage(requestMessage.getPayload(), amqpMessageProperties);
98+
amqpMessage = converter.toMessage(message.getPayload(), amqpMessageProperties);
6999
if (headersMappedLast) {
70-
headerMapper.fromHeadersToRequest(requestMessage.getHeaders(), amqpMessageProperties);
100+
mapHeaders(message.getHeaders(), amqpMessageProperties, headerMapper, reply);
71101
}
72-
checkDeliveryMode(requestMessage, amqpMessageProperties, defaultDeliveryMode);
102+
checkDeliveryMode(message, amqpMessageProperties, defaultDeliveryMode);
73103
return amqpMessage;
74104
}
75105

106+
private static void mapHeaders(MessageHeaders messageHeaders, MessageProperties amqpMessageProperties,
107+
AmqpHeaderMapper headerMapper, boolean reply) {
108+
109+
if (reply) {
110+
headerMapper.fromHeadersToReply(messageHeaders, amqpMessageProperties);
111+
}
112+
else {
113+
headerMapper.fromHeadersToRequest(messageHeaders, amqpMessageProperties);
114+
}
115+
}
116+
76117
private static String contentTypeAsString(MessageHeaders headers) {
77118
Object contentType = headers.get(AmqpHeaders.CONTENT_TYPE);
78119
if (contentType instanceof MimeType) {

spring-integration-amqp/src/main/resources/org/springframework/integration/amqp/config/spring-integration-amqp-5.1.xsd

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,18 @@
239239
</xsd:documentation>
240240
</xsd:annotation>
241241
</xsd:attribute>
242+
<xsd:attribute name="reply-headers-last">
243+
<xsd:annotation>
244+
<xsd:documentation>
245+
Whether reply headers are mapped before or after conversion from a messaging Message to
246+
a spring amqp Message. Set to true, for example, if you wish to override the
247+
contentType header set by the converter.
248+
</xsd:documentation>
249+
</xsd:annotation>
250+
<xsd:simpleType>
251+
<xsd:union memberTypes="xsd:boolean xsd:string" />
252+
</xsd:simpleType>
253+
</xsd:attribute>
242254
</xsd:extension>
243255
</xsd:complexContent>
244256
</xsd:complexType>

0 commit comments

Comments
 (0)