Skip to content

Commit ddc32a3

Browse files
garyrussellartembilan
authored andcommitted
GH-1382: Republish Recoverer Improvements
Resolves #1382 Add expressions; make private method protected. **cherry-pick to 2.4.x**
1 parent 06ba396 commit ddc32a3

File tree

3 files changed

+94
-17
lines changed

3 files changed

+94
-17
lines changed

spring-rabbit/src/main/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecoverer.java

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2021 the original author or authors.
2+
* Copyright 2014-2022 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.
@@ -29,6 +29,10 @@
2929
import org.springframework.amqp.core.MessageProperties;
3030
import org.springframework.amqp.rabbit.connection.RabbitUtils;
3131
import org.springframework.amqp.rabbit.core.RabbitTemplate;
32+
import org.springframework.expression.EvaluationContext;
33+
import org.springframework.expression.Expression;
34+
import org.springframework.expression.common.LiteralExpression;
35+
import org.springframework.expression.spel.support.StandardEvaluationContext;
3236
import org.springframework.lang.Nullable;
3337
import org.springframework.util.Assert;
3438

@@ -68,9 +72,11 @@ public class RepublishMessageRecoverer implements MessageRecoverer {
6872

6973
protected final AmqpTemplate errorTemplate; // NOSONAR
7074

71-
protected final String errorRoutingKey; // NOSONAR
75+
protected final Expression errorRoutingKeyExpression; // NOSONAR
7276

73-
protected final String errorExchangeName; // NOSONAR
77+
protected final Expression errorExchangeNameExpression; // NOSONAR
78+
79+
protected final EvaluationContext evaluationContext = new StandardEvaluationContext();
7480

7581
private String errorRoutingKeyPrefix = "error.";
7682

@@ -80,19 +86,48 @@ public class RepublishMessageRecoverer implements MessageRecoverer {
8086

8187
private MessageDeliveryMode deliveryMode = MessageDeliveryMode.PERSISTENT;
8288

89+
/**
90+
* Create an instance with the provided template.
91+
* @param errorTemplate the template.
92+
*/
8393
public RepublishMessageRecoverer(AmqpTemplate errorTemplate) {
84-
this(errorTemplate, null, null);
94+
this(errorTemplate, (String) null, (String) null);
8595
}
8696

97+
/**
98+
* Create an instance with the provided properties.
99+
* @param errorTemplate the template.
100+
* @param errorExchange the exchange.
101+
*/
87102
public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange) {
88103
this(errorTemplate, errorExchange, null);
89104
}
90105

106+
/**
107+
* Create an instance with the provided properties. If the exchange or routing key is null,
108+
* the template's default will be used.
109+
* @param errorTemplate the template.
110+
* @param errorExchange the exchange.
111+
* @param errorRoutingKey the routing key.
112+
*/
91113
public RepublishMessageRecoverer(AmqpTemplate errorTemplate, String errorExchange, String errorRoutingKey) {
114+
this(errorTemplate, new LiteralExpression(errorExchange), new LiteralExpression(errorRoutingKey));
115+
}
116+
117+
/**
118+
* Create an instance with the provided properties. If the exchange or routing key
119+
* evaluate to null, the template's default will be used.
120+
* @param errorTemplate the template.
121+
* @param errorExchange the exchange expression, evaluated against the message.
122+
* @param errorRoutingKey the routing key, evaluated against the message.
123+
*/
124+
public RepublishMessageRecoverer(AmqpTemplate errorTemplate, @Nullable Expression errorExchange,
125+
@Nullable Expression errorRoutingKey) {
126+
92127
Assert.notNull(errorTemplate, "'errorTemplate' cannot be null");
93128
this.errorTemplate = errorTemplate;
94-
this.errorExchangeName = errorExchange;
95-
this.errorRoutingKey = errorRoutingKey;
129+
this.errorExchangeNameExpression = errorExchange != null ? errorExchange : new LiteralExpression(null);
130+
this.errorRoutingKeyExpression = errorRoutingKey != null ? errorRoutingKey : new LiteralExpression(null);
96131
if (!(this.errorTemplate instanceof RabbitTemplate)) {
97132
this.maxStackTraceLength = Integer.MAX_VALUE;
98133
}
@@ -175,17 +210,17 @@ public void recover(Message message, Throwable cause) {
175210
messageProperties.setDeliveryMode(this.deliveryMode);
176211
}
177212

178-
if (null != this.errorExchangeName) {
179-
String routingKey = this.errorRoutingKey != null ? this.errorRoutingKey
180-
: this.prefixedOriginalRoutingKey(message);
181-
doSend(this.errorExchangeName, routingKey, message);
213+
String exchangeName = this.errorExchangeNameExpression.getValue(this.evaluationContext, message, String.class);
214+
String rk = this.errorRoutingKeyExpression.getValue(this.evaluationContext, message, String.class);
215+
String routingKey = rk != null ? rk : this.prefixedOriginalRoutingKey(message);
216+
if (null != exchangeName) {
217+
doSend(exchangeName, routingKey, message);
182218
if (this.logger.isWarnEnabled()) {
183-
this.logger.warn("Republishing failed message to exchange '" + this.errorExchangeName
219+
this.logger.warn("Republishing failed message to exchange '" + exchangeName
184220
+ "' with routing key " + routingKey);
185221
}
186222
}
187223
else {
188-
final String routingKey = this.prefixedOriginalRoutingKey(message);
189224
doSend(null, routingKey, message);
190225
if (this.logger.isWarnEnabled()) {
191226
this.logger.warn("Republishing failed message to the template's default exchange with routing key "
@@ -271,11 +306,24 @@ else if (stackTraceAsString.length() + exceptionMessage.length() > this.maxStack
271306
return null;
272307
}
273308

274-
private String prefixedOriginalRoutingKey(Message message) {
309+
/**
310+
* The default behavior of this method is to append the received routing key to the
311+
* {@link #setErrorRoutingKeyPrefix(String) routingKeyPrefix}. This is only invoked
312+
* if the routing key is null.
313+
* @param message the message.
314+
* @return the routing key.
315+
*/
316+
protected String prefixedOriginalRoutingKey(Message message) {
275317
return this.errorRoutingKeyPrefix + message.getMessageProperties().getReceivedRoutingKey();
276318
}
277319

278-
private String getStackTraceAsString(Throwable cause) {
320+
/**
321+
* Create a String representation of the stack trace.
322+
* @param cause the throwable.
323+
* @return the String.
324+
* @since 2.4.8
325+
*/
326+
protected String getStackTraceAsString(Throwable cause) {
279327
StringWriter stringWriter = new StringWriter();
280328
PrintWriter printWriter = new PrintWriter(stringWriter, true);
281329
cause.printStackTrace(printWriter);

spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTest.java renamed to spring-rabbit/src/test/java/org/springframework/amqp/rabbit/retry/RepublishMessageRecovererTests.java

Lines changed: 28 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-2022 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.
@@ -33,6 +33,7 @@
3333
import org.springframework.amqp.core.Message;
3434
import org.springframework.amqp.core.MessageDeliveryMode;
3535
import org.springframework.amqp.core.MessageProperties;
36+
import org.springframework.expression.spel.standard.SpelExpressionParser;
3637

3738
/**
3839
* @author James Carr
@@ -42,7 +43,7 @@
4243
* @since 1.3
4344
*/
4445
@ExtendWith(MockitoExtension.class)
45-
public class RepublishMessageRecovererTest {
46+
public class RepublishMessageRecovererTests {
4647

4748
private final Message message = new Message("".getBytes(), new MessageProperties());
4849

@@ -151,4 +152,29 @@ void setDeliveryModeIfNull() {
151152
assertThat(this.message.getMessageProperties().getDeliveryMode()).isEqualTo(MessageDeliveryMode.NON_PERSISTENT);
152153
}
153154

155+
@Test
156+
void dynamicExRk() {
157+
this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate,
158+
new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorExchange')"),
159+
new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')"));
160+
this.message.getMessageProperties().setHeader("errorExchange", "ex");
161+
this.message.getMessageProperties().setHeader("errorRK", "rk");
162+
163+
this.recoverer.recover(this.message, this.cause);
164+
165+
verify(this.amqpTemplate).send("ex", "rk", this.message);
166+
}
167+
168+
@Test
169+
void dynamicRk() {
170+
this.recoverer = new RepublishMessageRecoverer(this.amqpTemplate, null,
171+
new SpelExpressionParser().parseExpression("messageProperties.headers.get('errorRK')"));
172+
this.message.getMessageProperties().setHeader("errorExchange", "ex");
173+
this.message.getMessageProperties().setHeader("errorRK", "rk");
174+
175+
this.recoverer.recover(this.message, this.cause);
176+
177+
verify(this.amqpTemplate).send("rk", this.message);
178+
}
179+
154180
}

src/reference/asciidoc/amqp.adoc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6783,7 +6783,7 @@ The default `MessageRecoverer` consumes the errant message and emits a `WARN` me
67836783

67846784
Starting with version 1.3, a new `RepublishMessageRecoverer` is provided, to allow publishing of failed messages after retries are exhausted.
67856785

6786-
When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange, if any.
6786+
When a recoverer consumes the final exception, the message is ack'd and is not sent to the dead letter exchange by the broker, if configured.
67876787

67886788
NOTE: When `RepublishMessageRecoverer` is used on the consumer side, the received message has `deliveryMode` in the `receivedDeliveryMode` message property.
67896789
In this case the `deliveryMode` is `null`.
@@ -6834,6 +6834,9 @@ Starting with versions 2.1.13, 2.2.3, the exception message is included in this
68346834
* if the stack trace is small, the message will be truncated (plus `...`) to fit in the available bytes (but the message within the stack trace itself is truncated to 97 bytes plus `...`).
68356835

68366836
Whenever a truncation of any kind occurs, the original exception will be logged to retain the complete information.
6837+
The evaluation is performed after the headers are enhanced so information such as the exception type can be used in the expressions.
6838+
6839+
Starting with version 2.4.8, the error exchange and routing key can be provided as SpEL expressions, with the `Message` being the root object for the evaluation.
68376840

68386841
Starting with version 2.3.3, a new subclass `RepublishMessageRecovererWithConfirms` is provided; this supports both styles of publisher confirms and will wait for the confirmation before returning (or throw an exception if not confirmed or the message is returned).
68396842

0 commit comments

Comments
 (0)