Skip to content

Commit e4c523b

Browse files
garyrussellartembilan
authored andcommitted
Add @RabbitListener replyPostProcessor
- allow customization of the reply message based on the request message.
1 parent abec859 commit e4c523b

File tree

10 files changed

+155
-12
lines changed

10 files changed

+155
-12
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListener.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -263,4 +263,14 @@
263263
*/
264264
String ackMode() default "";
265265

266+
/**
267+
* The bean name of a
268+
* {@link org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor} to post
269+
* process a response before it is sent.
270+
* @return the bean name.
271+
* @since 2.2.5
272+
* @see org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener#setReplyPostProcessor(org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor)
273+
*/
274+
String replyPostProcessor() default "";
275+
266276
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/annotation/RabbitListenerAnnotationBeanPostProcessor.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
5252
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
5353
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
54+
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
5455
import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler;
5556
import org.springframework.aop.framework.Advised;
5657
import org.springframework.aop.support.AopUtils;
@@ -451,6 +452,7 @@ else if (errorHandler instanceof String) {
451452
resolveExecutor(endpoint, rabbitListener, target, beanName);
452453
resolveAdmin(endpoint, rabbitListener, target);
453454
resolveAckMode(endpoint, rabbitListener);
455+
resolvePostProcessor(endpoint, rabbitListener, target, beanName);
454456
RabbitListenerContainerFactory<?> factory = resolveContainerFactory(rabbitListener, target, beanName);
455457

456458
this.registrar.registerEndpoint(endpoint, factory);
@@ -519,8 +521,26 @@ private void resolveExecutor(MethodRabbitListenerEndpoint endpoint, RabbitListen
519521
}
520522
catch (NoSuchBeanDefinitionException ex) {
521523
throw new BeanInitializationException("Could not register rabbit listener endpoint on ["
522-
+ execTarget + "] for bean " + beanName + ", no " + TaskExecutor.class.getSimpleName()
523-
+ " with id '" + execBeanName + "' was found in the application context", ex);
524+
+ execTarget + "] for bean " + beanName + ", no 'TaskExecutor' with id '"
525+
+ execBeanName + "' was found in the application context", ex);
526+
}
527+
}
528+
}
529+
530+
@SuppressWarnings("unchecked")
531+
private void resolvePostProcessor(MethodRabbitListenerEndpoint endpoint, RabbitListener rabbitListener,
532+
Object target, String beanName) {
533+
534+
String ppBeanName = resolve(rabbitListener.replyPostProcessor());
535+
if (StringUtils.hasText(ppBeanName)) {
536+
Assert.state(this.beanFactory != null, "BeanFactory must be set to obtain container factory by bean name");
537+
try {
538+
endpoint.setReplyPostProcessor(this.beanFactory.getBean(ppBeanName, ReplyPostProcessor.class));
539+
}
540+
catch (NoSuchBeanDefinitionException ex) {
541+
throw new BeanInitializationException("Could not register rabbit listener endpoint on ["
542+
+ target + "] for bean " + beanName + ", no 'ReplyPostProcessor' with id '"
543+
+ ppBeanName + "' was found in the application context", ex);
524544
}
525545
}
526546
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/config/AbstractRabbitListenerContainerFactory.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -455,7 +455,8 @@ public C createListenerContainer(RabbitListenerEndpoint endpoint) {
455455
.acceptIfNotNull(this.retryTemplate, messageListener::setRetryTemplate)
456456
.acceptIfCondition(this.retryTemplate != null && this.recoveryCallback != null,
457457
this.recoveryCallback, messageListener::setRecoveryCallback)
458-
.acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected);
458+
.acceptIfNotNull(this.defaultRequeueRejected, messageListener::setDefaultRequeueRejected)
459+
.acceptIfNotNull(endpoint.getReplyPostProcessor(), messageListener::setReplyPostProcessor);
459460
}
460461
initializeContainer(instance, endpoint);
461462

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractRabbitListenerEndpoint.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
import org.springframework.amqp.core.MessageListener;
2828
import org.springframework.amqp.core.Queue;
2929
import org.springframework.amqp.rabbit.batch.BatchingStrategy;
30+
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
3031
import org.springframework.amqp.support.converter.MessageConverter;
3132
import org.springframework.beans.BeansException;
3233
import org.springframework.beans.factory.BeanFactory;
@@ -90,6 +91,8 @@ public abstract class AbstractRabbitListenerEndpoint implements RabbitListenerEn
9091

9192
private AcknowledgeMode ackMode;
9293

94+
private ReplyPostProcessor replyPostProcessor;
95+
9396
@Override
9497
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
9598
this.beanFactory = beanFactory;
@@ -318,8 +321,22 @@ public AcknowledgeMode getAckMode() {
318321
return this.ackMode;
319322
}
320323

321-
public void setAckMode(AcknowledgeMode ackMode) {
322-
this.ackMode = ackMode;
324+
public void setAckMode(AcknowledgeMode mode) {
325+
this.ackMode = mode;
326+
}
327+
328+
@Override
329+
public ReplyPostProcessor getReplyPostProcessor() {
330+
return this.replyPostProcessor;
331+
}
332+
333+
/**
334+
* Set a {@link ReplyPostProcessor} to post process a response message before it is sent.
335+
* @param replyPostProcessor the post processor.
336+
* @since 2.2.5
337+
*/
338+
public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) {
339+
this.replyPostProcessor = replyPostProcessor;
323340
}
324341

325342
@Override

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/RabbitListenerEndpoint.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import org.springframework.amqp.core.AcknowledgeMode;
2020
import org.springframework.amqp.rabbit.batch.BatchingStrategy;
21+
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
2122
import org.springframework.amqp.support.converter.MessageConverter;
2223
import org.springframework.core.task.TaskExecutor;
2324
import org.springframework.lang.Nullable;
@@ -136,4 +137,15 @@ default AcknowledgeMode getAckMode() {
136137
return null;
137138
}
138139

140+
/**
141+
* Return a {@link ReplyPostProcessor} to post process a reply message before it is
142+
* sent.
143+
* @return the post processor.
144+
* @since 2.2.5
145+
*/
146+
@Nullable
147+
default ReplyPostProcessor getReplyPostProcessor() {
148+
return null;
149+
}
150+
139151
}

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/adapter/AbstractAdaptableMessageListener.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -117,6 +117,8 @@ public abstract class AbstractAdaptableMessageListener implements ChannelAwareMe
117117

118118
private boolean defaultRequeueRejected = true;
119119

120+
private ReplyPostProcessor replyPostProcessor;
121+
120122
/**
121123
* Set the routing key to use when sending response messages.
122124
* This will be applied in case of a request message that
@@ -242,6 +244,17 @@ public void setBeanResolver(BeanResolver beanResolver) {
242244
this.evalContext.addPropertyAccessor(new MapAccessor());
243245
}
244246

247+
/**
248+
* Set a {@link ReplyPostProcessor} to post process a response message before it is
249+
* sent. It is called after {@link #postProcessResponse(Message, Message)} which sets
250+
* up the correlationId header.
251+
* @param replyPostProcessor the post processor.
252+
* @since 2.2.5
253+
*/
254+
public void setReplyPostProcessor(ReplyPostProcessor replyPostProcessor) {
255+
this.replyPostProcessor = replyPostProcessor;
256+
}
257+
245258
/**
246259
* Return the converter that will convert incoming Rabbit messages to listener method arguments, and objects
247260
* returned from listener methods back to Rabbit messages.
@@ -413,6 +426,9 @@ protected void doHandleResult(InvocationResult resultArg, Message request, Chann
413426
props.setTargetBean(resultArg.getBean());
414427
props.setTargetMethod(resultArg.getMethod());
415428
postProcessResponse(request, response);
429+
if (this.replyPostProcessor != null) {
430+
response = this.replyPostProcessor.apply(request, response);
431+
}
416432
Address replyTo = getReplyToAddress(request, source, resultArg);
417433
sendResponse(channel, replyTo, response);
418434
}
@@ -529,6 +545,7 @@ private Address evaluateReplyTo(Message request, Object source, Object result, E
529545
* @param replyTo the Rabbit ReplyTo string to use when sending. Currently interpreted to be the routing key.
530546
* @param messageIn the Rabbit message to send
531547
* @see #postProcessResponse(Message, Message)
548+
* @see #setReplyPostProcessor(ReplyPostProcessor)
532549
*/
533550
protected void sendResponse(Channel channel, Address replyTo, Message messageIn) {
534551
Message message = messageIn;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.amqp.rabbit.listener.adapter;
18+
19+
import java.util.function.BiFunction;
20+
21+
import org.springframework.amqp.core.Message;
22+
23+
/**
24+
* A post processor for replies. The first parameter to the function is the request
25+
* message, the second is the response message; it must return the modified (or a new)
26+
* message. Use this, for example, if you want to copy additional headers from the request
27+
* message.
28+
*
29+
* @author Gary Russell
30+
* @since 2.2.5
31+
*
32+
*/
33+
public interface ReplyPostProcessor extends BiFunction<Message, Message, Message> {
34+
35+
}

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/annotation/EnableRabbitIntegrationTests.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2019 the original author or authors.
2+
* Copyright 2014-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -77,6 +77,7 @@
7777
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistrar;
7878
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
7979
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
80+
import org.springframework.amqp.rabbit.listener.adapter.ReplyPostProcessor;
8081
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
8182
import org.springframework.amqp.rabbit.listener.api.RabbitListenerErrorHandler;
8283
import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException;
@@ -417,6 +418,7 @@ public void endpointWithHeader() {
417418
.isEqualTo("MyService");
418419
assertThat((String) reply.getMessageProperties().getHeader("method"))
419420
.isEqualTo("capitalizeWithHeader");
421+
assertThat((String) reply.getMessageProperties().getHeader("prefix")).isEqualTo("prefix-");
420422
}
421423

422424
@Test
@@ -1089,7 +1091,7 @@ public String multiQueuesConfig(String foo) {
10891091
return foo.toUpperCase() + foo;
10901092
}
10911093

1092-
@RabbitListener(queues = "test.header", group = "testGroup")
1094+
@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoPrefixHeader")
10931095
public String capitalizeWithHeader(@Payload String content, @Header String prefix) {
10941096
return prefix + content.toUpperCase();
10951097
}
@@ -1806,6 +1808,14 @@ public DirectExchange internal() {
18061808
return directExchange;
18071809
}
18081810

1811+
@Bean
1812+
public ReplyPostProcessor echoPrefixHeader() {
1813+
return (req, resp) -> {
1814+
resp.getMessageProperties().setHeader("prefix", req.getMessageProperties().getHeader("prefix"));
1815+
return resp;
1816+
};
1817+
}
1818+
18091819
}
18101820

18111821
@RabbitListener(bindings = @QueueBinding

src/reference/asciidoc/amqp.adoc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2635,7 +2635,25 @@ factory.setBeforeSendReplyPostProcessors(msg -> {
26352635
----
26362636
====
26372637

2638+
Starting with version 2.2.5, you can configure a `ReplyPostProcessor` to modify the reply message before it is sent; it is called after the `correlationId` header has been set up to match the request.
26382639

2640+
====
2641+
[source, java]
2642+
----
2643+
@RabbitListener(queues = "test.header", group = "testGroup", replyPostProcessor = "echoCustomHeader")
2644+
public String capitalizeWithHeader(String in) {
2645+
return in.toUpperCase();
2646+
}
2647+
2648+
@Bean
2649+
public ReplyPostProcessor echoCustomHeader() {
2650+
return (req, resp) -> {
2651+
resp.getMessageProperties().setHeader("myHeader", req.getMessageProperties().getHeader("myHeader"));
2652+
return resp;
2653+
};
2654+
}
2655+
----
2656+
====
26392657

26402658
The `@SendTo` value is assumed as a reply `exchange` and `routingKey` pair that follws the `exchange/routingKey` pattern,
26412659
where one of those parts can be omitted.

src/reference/asciidoc/whats-new.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ When a `@RabbitListener` method returns a result, the bean and `Method` are now
6363
This allows configuration of a `beforeSendReplyMessagePostProcessor` to, for example, set a header in the reply to indicate which method was invoked on the server.
6464
See <<async-annotation-driven-reply>> for more information.
6565

66+
You can now configure a `ReplyPostProcessor` to make modifications to a reply message before it is sent.
67+
See <<async-annotation-driven-reply>> for more information.
68+
6669
==== AMQP Logging Appenders Changes
6770

6871
The Log4J and Logback `AmqpAppender` s now support a `verifyHostname` SSL option.

0 commit comments

Comments
 (0)