Skip to content

Commit 5be96cb

Browse files
committed
GH-2994: Add RabbitAmqpListenerContainer infrastructure
Related to: #2994 * Add `RabbitAmqpMessageListener` for RabbitMQ AMQP 1.0 native message consumption * Add `RabbitAmqpMessageListenerAdapter` for `@RabbitLister` API * Add `RabbitAmqpListenerContainer` and respective `RabbitAmqpListenerContainerFactory` * Add `AmqpAcknowledgment` as a general abstraction. In the `RabbitAmqpListenerContainer` delegates to the `Consumer.Context` for manual settlement * Extract `RabbitAmqpUtils` for conversion to/from AMQP 1.0 native message * Add convenient `ContainerUtils.isImmediateAcknowledge()` and `ContainerUtils.isAmqpReject()` utilities * Expose `AmqpAcknowledgment` as a `MessageProperties.amqpAcknowledgment` for generic `MessageListener` use-cases * Remove `io.micrometer` dependecies from the `spring-rabbitmq-client` module since metrics and observation handled thoroughly in the `com.rabbitmq.client:amqp-client` Not tests for the listener yet. Therefore no fixing for the issue.
1 parent 6f0a338 commit 5be96cb

File tree

13 files changed

+888
-97
lines changed

13 files changed

+888
-97
lines changed

build.gradle

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,6 @@ project('spring-rabbitmq-client') {
479479
dependencies {
480480
api project(':spring-rabbit')
481481
api "com.rabbitmq.client:amqp-client:$rabbitmqAmqpClientVersion"
482-
api 'io.micrometer:micrometer-observation'
483482

484483
testApi project(':spring-rabbit-junit')
485484

@@ -488,10 +487,6 @@ project('spring-rabbitmq-client') {
488487
testImplementation 'org.testcontainers:rabbitmq'
489488
testImplementation 'org.testcontainers:junit-jupiter'
490489
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl'
491-
testImplementation 'io.micrometer:micrometer-observation-test'
492-
testImplementation 'io.micrometer:micrometer-tracing-bridge-brave'
493-
testImplementation 'io.micrometer:micrometer-tracing-test'
494-
testImplementation 'io.micrometer:micrometer-tracing-integration-test'
495490
}
496491
}
497492

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2025 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.core;
18+
19+
/**
20+
* An abstraction over acknowledgments.
21+
*
22+
* @author Artem Bilan
23+
*
24+
* @since 4.0
25+
*/
26+
@FunctionalInterface
27+
public interface AmqpAcknowledgment {
28+
29+
/**
30+
* Acknowledge the message.
31+
* @param status the status.
32+
*/
33+
void acknowledge(Status status);
34+
35+
default void acknowledge() {
36+
acknowledge(Status.ACCEPT);
37+
}
38+
39+
enum Status {
40+
41+
/**
42+
* Mark the message as accepted.
43+
*/
44+
ACCEPT,
45+
46+
/**
47+
* Mark the message as rejected.
48+
*/
49+
REJECT,
50+
51+
/**
52+
* Reject the message and requeue so that it will be redelivered.
53+
*/
54+
REQUEUE
55+
56+
}
57+
58+
}

spring-amqp/src/main/java/org/springframework/amqp/core/MessageProperties.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ public class MessageProperties implements Serializable {
157157

158158
private transient @Nullable Object targetBean;
159159

160+
private transient @Nullable AmqpAcknowledgment amqpAcknowledgment;
161+
160162
public void setHeader(String key, Object value) {
161163
this.headers.put(key, value);
162164
}
@@ -641,6 +643,25 @@ public void setProjectionUsed(boolean projectionUsed) {
641643
}
642644
}
643645

646+
/**
647+
* Return the {@link AmqpAcknowledgment} for consumer if any.
648+
* @return the {@link AmqpAcknowledgment} for consumer if any.
649+
* @since 4.0
650+
*/
651+
public @Nullable AmqpAcknowledgment getAmqpAcknowledgment() {
652+
return this.amqpAcknowledgment;
653+
}
654+
655+
/**
656+
* Set an {@link AmqpAcknowledgment} for manual acks in the target message processor.
657+
* This is only in-application a consumer side logic.
658+
* @param amqpAcknowledgment the {@link AmqpAcknowledgment} to use in the application.
659+
* @since 4.0
660+
*/
661+
public void setAmqpAcknowledgment(AmqpAcknowledgment amqpAcknowledgment) {
662+
this.amqpAcknowledgment = amqpAcknowledgment;
663+
}
664+
644665
@Override // NOSONAR complexity
645666
public int hashCode() {
646667
final int prime = 31;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ protected boolean isNoLocal() {
752752
* to be sent to the dead letter exchange. Setting to false causes all rejections to not
753753
* be requeued. When true, the default can be overridden by the listener throwing an
754754
* {@link AmqpRejectAndDontRequeueException}. Default true.
755-
* @param defaultRequeueRejected true to reject by default.
755+
* @param defaultRequeueRejected true to requeue by default.
756756
*/
757757
public void setDefaultRequeueRejected(boolean defaultRequeueRejected) {
758758
this.defaultRequeueRejected = defaultRequeueRejected;

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

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018-2022 the original author or authors.
2+
* Copyright 2018-2025 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.
@@ -17,8 +17,10 @@
1717
package org.springframework.amqp.rabbit.listener.support;
1818

1919
import org.apache.commons.logging.Log;
20+
import org.jspecify.annotations.Nullable;
2021

2122
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
23+
import org.springframework.amqp.ImmediateAcknowledgeAmqpException;
2224
import org.springframework.amqp.ImmediateRequeueAmqpException;
2325
import org.springframework.amqp.rabbit.listener.exception.MessageRejectedWhileStoppingException;
2426

@@ -59,7 +61,11 @@ else if (t instanceof ImmediateRequeueAmqpException) {
5961
shouldRequeue = true;
6062
break;
6163
}
62-
t = t.getCause();
64+
Throwable cause = t.getCause();
65+
if (cause == t) {
66+
break;
67+
}
68+
t = cause;
6369
}
6470
if (logger.isDebugEnabled()) {
6571
logger.debug("Rejecting messages (requeue=" + shouldRequeue + ")");
@@ -75,8 +81,41 @@ else if (t instanceof ImmediateRequeueAmqpException) {
7581
* @since 2.2
7682
*/
7783
public static boolean isRejectManual(Throwable ex) {
78-
return ex instanceof AmqpRejectAndDontRequeueException aradrex
79-
&& aradrex.isRejectManual();
84+
AmqpRejectAndDontRequeueException amqpRejectAndDontRequeueException =
85+
findInCause(ex, AmqpRejectAndDontRequeueException.class);
86+
return amqpRejectAndDontRequeueException != null && amqpRejectAndDontRequeueException.isRejectManual();
87+
}
88+
89+
/**
90+
* Return true for {@link ImmediateAcknowledgeAmqpException}.
91+
* @param ex the exception to traverse.
92+
* @return true if an {@link ImmediateAcknowledgeAmqpException} is present in the cause chain.
93+
* @since 4.0
94+
*/
95+
public static boolean isImmediateAcknowledge(Throwable ex) {
96+
return findInCause(ex, ImmediateAcknowledgeAmqpException.class) != null;
97+
}
98+
99+
/**
100+
* Return true for {@link AmqpRejectAndDontRequeueException}.
101+
* @param ex the exception to traverse.
102+
* @return true if an {@link AmqpRejectAndDontRequeueException} is present in the cause chain.
103+
* @since 4.0
104+
*/
105+
public static boolean isAmqpReject(Throwable ex) {
106+
return findInCause(ex, AmqpRejectAndDontRequeueException.class) != null;
107+
}
108+
109+
@SuppressWarnings("unchecked")
110+
private static <T extends Throwable> @Nullable T findInCause(Throwable throwable, Class<T> exceptionToFind) {
111+
if (exceptionToFind.isAssignableFrom(throwable.getClass())) {
112+
return (T) throwable;
113+
}
114+
Throwable cause = throwable.getCause();
115+
if (cause == null || cause == throwable) {
116+
return null;
117+
}
118+
return findInCause(cause, exceptionToFind);
80119
}
81120

82121
}

spring-rabbitmq-client/src/main/java/org/springframework/amqp/rabbitmq/client/RabbitAmqpTemplate.java

Lines changed: 7 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616

1717
package org.springframework.amqp.rabbitmq.client;
1818

19-
import java.nio.charset.StandardCharsets;
2019
import java.time.Duration;
21-
import java.util.Date;
22-
import java.util.Map;
23-
import java.util.UUID;
2420
import java.util.concurrent.CompletableFuture;
2521
import java.util.concurrent.TimeUnit;
2622

@@ -182,38 +178,20 @@ public CompletableFuture<Boolean> send(String exchange, @Nullable String routing
182178
private CompletableFuture<Boolean> doSend(@Nullable String exchange, @Nullable String routingKey,
183179
@Nullable String queue, Message message) {
184180

185-
MessageProperties messageProperties = message.getMessageProperties();
186-
187-
com.rabbitmq.client.amqp.Message amqpMessage =
188-
this.publisher.message(message.getBody())
189-
.contentEncoding(messageProperties.getContentEncoding())
190-
.contentType(messageProperties.getContentType())
191-
.messageId(messageProperties.getMessageId())
192-
.correlationId(messageProperties.getCorrelationId())
193-
.priority(messageProperties.getPriority().byteValue())
194-
.replyTo(messageProperties.getReplyTo());
195-
181+
com.rabbitmq.client.amqp.Message amqpMessage = this.publisher.message();
196182
com.rabbitmq.client.amqp.Message.MessageAddressBuilder address = amqpMessage.toAddress();
197-
198-
Map<String, @Nullable Object> headers = messageProperties.getHeaders();
199-
if (!headers.isEmpty()) {
200-
headers.forEach((key, val) -> mapProp(key, val, amqpMessage));
201-
}
202-
203183
JavaUtils.INSTANCE
204-
.acceptIfNotNull(messageProperties.getUserId(),
205-
(userId) -> amqpMessage.userId(userId.getBytes(StandardCharsets.UTF_8)))
206-
.acceptIfNotNull(messageProperties.getTimestamp(),
207-
(timestamp) -> amqpMessage.creationTime(timestamp.getTime()))
208-
.acceptIfNotNull(messageProperties.getExpiration(),
209-
(expiration) -> amqpMessage.absoluteExpiryTime(Long.parseLong(expiration)))
210184
.acceptIfNotNull(exchange, address::exchange)
211185
.acceptIfNotNull(routingKey, address::key)
212186
.acceptIfNotNull(queue, address::queue);
213187

188+
amqpMessage = address.message();
189+
190+
RabbitAmqpUtils.toAmqpMessage(message, amqpMessage);
191+
214192
CompletableFuture<Boolean> publishResult = new CompletableFuture<>();
215193

216-
this.publisher.publish(address.message(),
194+
this.publisher.publish(amqpMessage,
217195
(context) -> {
218196
switch (context.status()) {
219197
case ACCEPTED -> publishResult.complete(true);
@@ -299,7 +277,7 @@ public CompletableFuture<Message> receive(String queueName) {
299277
.priority(10)
300278
.messageHandler((context, message) -> {
301279
context.accept();
302-
messageFuture.complete(fromAmqpMessage(message));
280+
messageFuture.complete(RabbitAmqpUtils.fromAmqpMessage(message, null));
303281
})
304282
.build();
305283

@@ -452,62 +430,4 @@ public <C> CompletableFuture<C> convertSendAndReceiveAsType(String exchange, Str
452430
throw new UnsupportedOperationException();
453431
}
454432

455-
private static void mapProp(String key, @Nullable Object val, com.rabbitmq.client.amqp.Message amqpMessage) {
456-
if (val == null) {
457-
return;
458-
}
459-
if (val instanceof String string) {
460-
amqpMessage.property(key, string);
461-
}
462-
else if (val instanceof Long longValue) {
463-
amqpMessage.property(key, longValue);
464-
}
465-
else if (val instanceof Integer intValue) {
466-
amqpMessage.property(key, intValue);
467-
}
468-
else if (val instanceof Short shortValue) {
469-
amqpMessage.property(key, shortValue);
470-
}
471-
else if (val instanceof Byte byteValue) {
472-
amqpMessage.property(key, byteValue);
473-
}
474-
else if (val instanceof Double doubleValue) {
475-
amqpMessage.property(key, doubleValue);
476-
}
477-
else if (val instanceof Float floatValue) {
478-
amqpMessage.property(key, floatValue);
479-
}
480-
else if (val instanceof Character character) {
481-
amqpMessage.property(key, character);
482-
}
483-
else if (val instanceof UUID uuid) {
484-
amqpMessage.property(key, uuid);
485-
}
486-
else if (val instanceof byte[] bytes) {
487-
amqpMessage.property(key, bytes);
488-
}
489-
else if (val instanceof Boolean booleanValue) {
490-
amqpMessage.property(key, booleanValue);
491-
}
492-
}
493-
494-
private static Message fromAmqpMessage(com.rabbitmq.client.amqp.Message amqpMessage) {
495-
MessageProperties messageProperties = new MessageProperties();
496-
497-
JavaUtils.INSTANCE
498-
.acceptIfNotNull(amqpMessage.messageIdAsString(), messageProperties::setMessageId)
499-
.acceptIfNotNull(amqpMessage.userId(),
500-
(usr) -> messageProperties.setUserId(new String(usr, StandardCharsets.UTF_8)))
501-
.acceptIfNotNull(amqpMessage.correlationIdAsString(), messageProperties::setCorrelationId)
502-
.acceptIfNotNull(amqpMessage.contentType(), messageProperties::setContentType)
503-
.acceptIfNotNull(amqpMessage.contentEncoding(), messageProperties::setContentEncoding)
504-
.acceptIfNotNull(amqpMessage.absoluteExpiryTime(),
505-
(exp) -> messageProperties.setExpiration(Long.toString(exp)))
506-
.acceptIfNotNull(amqpMessage.creationTime(), (time) -> messageProperties.setTimestamp(new Date(time)));
507-
508-
amqpMessage.forEachProperty(messageProperties::setHeader);
509-
510-
return new Message(amqpMessage.body(), messageProperties);
511-
}
512-
513433
}

0 commit comments

Comments
 (0)