Spring Boot: RabbitMQ
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Every other RabbitMQ page in this section — RabbitMQ Architecture and the AMQP Model,
Publishing to RabbitMQ,
Consuming from RabbitMQ,
RabbitMQ Queue Types,
RabbitMQ Routing Patterns,
RabbitMQ Reliability, Dead-Lettering and
TTL and RabbitMQ and AMQP 1.0 — works directly against the
com.rabbitmq.client Java client. This page is the Spring layer on top of all of them: how the same exchanges,
queues, bindings, publisher confirms, acknowledgement modes and AMQP 1.0 concepts those pages already explained
show up as Spring beans, annotations and spring.rabbitmq.* properties once spring-boot-starter-amqp is on the
classpath. It assumes that protocol-level vocabulary throughout and cross-references it instead of repeating it.
The baseline is Spring Boot 4.1.x on Spring Framework 7.1.x, Spring AMQP 4.1.x (spring-rabbit), against a
RabbitMQ 4.3.x broker, running on a Java 25 runtime with Java 21 as the current LTS floor. Spring AMQP 4.0
deprecated the Jackson 2-based converter family for removal and 4.1 replaced it outright with Jackson 3-based
types, which matters concretely below; every class, method and property name here has been checked against the
current docs.spring.io reference rather than carried over from older Spring Boot 1.x/2.x material, since a good
deal of published RabbitMQ-with-Spring-Boot content (including the RabbitMQ chapter of a well-known 2017 book
written against Spring Boot 1.4 and RabbitMQ 3.6) predates the current container types, converters, property
names and even the removal of classic mirrored queues covered on
RabbitMQ Queue Types.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
spring-boot-starter-amqp brings in spring-rabbit and the RabbitMQ Java client, and triggers Spring Boot’s
RabbitAutoConfiguration, which wires a CachingConnectionFactory, a RabbitTemplate, a RabbitAdmin and a
default SimpleRabbitListenerContainerFactory from spring.rabbitmq.* properties, all without any @Bean
method of your own.
Dependency and connection configuration
| Property | Purpose |
|---|---|
|
A comma-separated list of |
|
Single-node connection details, used only when |
|
The vhost to connect to. |
|
Turns TLS on; |
|
Passed through to the underlying RabbitMQ Java |
spring:
rabbitmq:
addresses: node1.rabbit.internal:5671,node2.rabbit.internal:5671,node3.rabbit.internal:5671
virtual-host: orders
username: orders-service
password: ${RABBITMQ_PASSWORD}
ssl:
enabled: true
bundle: rabbitmq
See Spring Boot Reference — AMQP for the
complete auto-configuration this feeds, including RabbitConnectionDetails (used to source these values from a
ServiceConnection instead of plain properties in a container/Testcontainers setup) and the SSL bundle
mechanism.
Sending messages: RabbitTemplate, confirms and returns
AmqpTemplate/RabbitTemplate mirror channel.basicPublish from
Publishing to RabbitMQ one level up: send(exchange, routingKey,
message) takes a raw org.springframework.amqp.core.Message, and convertAndSend(exchange, routingKey, object)
runs the payload through the configured MessageConverter (below) first. Both default to whatever
template.exchange/template.routing-key are set to when the exchange/routing key arguments are omitted.
@Service
public class OrderPublisher {
private final RabbitTemplate rabbitTemplate;
public OrderPublisher(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void publish(Order order) {
rabbitTemplate.convertAndSend("orders", "orders.eu.created", order);
}
}
Publisher confirms and returns — the same two mechanisms
Publishing to RabbitMQ covers at the AMQP level (confirmSelect,
the mandatory flag) — are turned on declaratively:
spring:
rabbitmq:
publisher-confirm-type: correlated # wraps the underlying channel in publisher-confirms mode
publisher-returns: true # enables basic.return callbacks for unroutable messages
template:
mandatory: true # this template's sends set the mandatory flag
retry:
enabled: true # retries a send if the broker connection is temporarily unavailable
initial-interval: 2s
max-retries: 3
With those set, a RabbitTemplate registers exactly one ConfirmCallback and one ReturnsCallback each, invoked
on one of the connection factory’s own executor threads rather than the AMQP client’s I/O thread:
@Configuration
public class RabbitTemplateConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
MessageConverter messageConverter) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMessageConverter(messageConverter);
template.setMandatory(true);
// confirm: broker accepted (ack=true) or could not handle (ack=false, cause explains why) the publish
template.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
log.warn("publish not confirmed, correlationId={}, cause={}",
correlationData != null ? correlationData.getId() : null, cause);
}
});
// return: mandatory=true and the exchange could not route the message to any queue
template.setReturnsCallback(returned ->
log.warn("message returned, exchange={}, routingKey={}, replyText={}",
returned.getExchange(), returned.getRoutingKey(), returned.getReplyText()));
return template;
}
}
Correlating a specific send with its confirm uses CorrelationData, passed as an extra argument to send/
convertAndSend; its own getFuture() returns a CompletableFuture<Confirm> as an alternative to the shared
ConfirmCallback, and when both confirms and returns are enabled, a returned message ends up on
correlationData.getReturn() too, guaranteed to be set before the confirm future completes:
CorrelationData correlationData = new CorrelationData(order.id());
rabbitTemplate.convertAndSend("orders", "orders.eu.created", order, correlationData);
correlationData.getFuture().whenComplete((confirm, ex) -> {
if (ex != null || !confirm.isAck()) {
log.warn("order {} not confirmed: {}", order.id(), confirm != null ? confirm.getReason() : ex);
}
});
See Spring AMQP Reference — Sending
Messages and Spring AMQP Reference — Using
RabbitTemplate (its "Correlated Publisher Confirms and Returns" section) for the full ConfirmCallback/
ReturnsCallback/CorrelationData contract this builds on.
Declaring topology as Spring beans
Exchanges, queues and bindings declared as @Bean`s are picked up automatically by the auto-configured
`RabbitAdmin and (re-)declared to the broker on every connection, including after a connection recovery — no
separate AmqpAdmin.declareQueue(…) call needed for anything defined this way.
@Configuration
public class OrdersTopologyConfig {
@Bean
public TopicExchange ordersExchange() {
return new TopicExchange("orders");
}
@Bean
public Queue ordersQueue() {
return QueueBuilder.durable("orders")
.quorum() // x-queue-type=quorum -- see RabbitMQ Queue Types
.deadLetterExchange("orders.dlx")
.build();
}
@Bean
public Binding ordersBinding() {
return BindingBuilder.bind(ordersQueue()).to(ordersExchange()).with("orders.*");
}
}
QueueBuilder and ExchangeBuilder are the fluent equivalent of the constructors
RabbitMQ Architecture and the AMQP Model
introduces at the AMQP level: QueueBuilder.durable(name)/.nonDurable(name) starts the chain, .quorum()/
.stream() set x-queue-type the same way RabbitMQ Queue
Types does directly, and well-known arguments (ttl, maxLength, deadLetterExchange, maxPriority,
singleActiveConsumer, and so on) are typed builder methods instead of a raw Map<String, Object>.
DirectExchange, TopicExchange, FanoutExchange and HeadersExchange map directly onto the four exchange
types from that same page, and BindingBuilder.bind(queue).to(exchange).with(routingKey) (or .matching(…)
for a headers exchange) replaces channel.queueBind.
Grouping several Declarable beans — queues, exchanges and bindings together — under one Declarables bean
avoids one @Bean method per object where that gets repetitive, and Declarables#getDeclarablesByType retrieves
a typed subset back out of it (useful when wiring a listener container’s setQueues(…) from the same beans the
admin already declared):
@Bean
public Declarables ordersTopology() {
return new Declarables(
new TopicExchange("orders"),
QueueBuilder.durable("orders").quorum().build(),
QueueBuilder.durable("orders.dlq").build(),
BindingBuilder.bind(new Queue("orders")).to(new TopicExchange("orders")).with("orders.*"));
}
Injecting AmqpAdmin directly is for anything genuinely dynamic — a queue whose name or arguments are only
known at runtime, not declarable as a fixed bean:
@Component
public class TenantQueueProvisioner {
private final AmqpAdmin amqpAdmin;
public TenantQueueProvisioner(AmqpAdmin amqpAdmin) {
this.amqpAdmin = amqpAdmin;
}
public void provisionQueueFor(String tenantId) {
amqpAdmin.declareQueue(QueueBuilder.durable("orders." + tenantId).quorum().build());
}
}
See Spring AMQP Reference — Broker
Configuration for the complete QueueBuilder/ExchangeBuilder/BindingBuilder/Declarables/AmqpAdmin API,
including conditional declaration across multiple RabbitAdmin instances (relevant to Multiple brokers
below).
Consuming messages: @RabbitListener
@RabbitListener is the annotated equivalent of channel.basicConsume from
Consuming from RabbitMQ. Its simplest form just names an
already-declared queue:
@Component
public class OrderListener {
@RabbitListener(queues = "orders")
public void onOrder(Order order) {
process(order);
}
}
bindings = @QueueBinding(…) folds the topology declaration from the previous section into the listener
itself — convenient for a self-contained listener, at the cost of spreading topology across annotations instead
of keeping it in one @Configuration class:
@Component
public class OrderListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "orders", durable = "true"),
exchange = @Exchange(value = "orders", type = ExchangeTypes.TOPIC),
key = "orders.*"))
public void onOrder(Order order) {
process(order);
}
}
containerFactory selects which RabbitListenerContainerFactory bean backs this listener (see the container
comparison below); concurrency sets the invoker thread count, either a fixed number or a "min-max" range
string such as "2-4" for a SimpleMessageListenerContainer to scale within; id names the underlying listener
container bean for later lookup through RabbitListenerEndpointRegistry; errorHandler names a
RabbitListenerErrorHandler bean invoked with the raw message, the converted Message<?> and the wrapping
ListenerExecutionFailedException, which can itself return a reply or rethrow; and returnExceptions, when
true, sends a thrown exception back to the sender (wrapped in a RemoteInvocationResult) instead of only
surfacing it to the container.
@RabbitListener(queues = "orders", containerFactory = "quorumContainerFactory",
concurrency = "2-4", errorHandler = "orderListenerErrorHandler")
public void onOrder(Order order) {
process(order);
}
Acknowledgement mode itself is not an @RabbitListener attribute on the classic AMQP 0-9-1 path — it is a
property of the container factory (setAcknowledgeMode(AcknowledgeMode.MANUAL), or
spring.rabbitmq.listener.simple.acknowledge-mode/.direct.acknowledge-mode), consistently with
AUTO/MANUAL/NONE mapping onto the automatic/manual/no-ack modes
Consuming from RabbitMQ covers at the AMQP level. Under manual
acknowledgement, a listener method’s own thrown exception is what tells the container to
basic.nack/basic.reject; throwing AmqpRejectAndDontRequeueException specifically forces
requeue=false regardless of the container’s defaultRequeueRejected setting, which is exactly how a listener
opts a message out of redelivery and into a configured dead-letter exchange (see
retry and dead lettering below).
Multiple methods on one listener: @RabbitHandler
A single @RabbitListener at the class level, combined with @RabbitHandler on several methods, lets one
listener endpoint dispatch to a different method by payload type after conversion — useful when one queue
legitimately carries more than one message shape:
@RabbitListener(queues = "orders.events")
@Component
public class OrderEventListener {
@RabbitHandler
public void onCreated(OrderCreated event) {
process(event);
}
@RabbitHandler
public void onCancelled(OrderCancelled event) {
process(event);
}
@RabbitHandler(isDefault = true)
public void onUnknown(Object event) {
log.warn("unrecognized order event: {}", event);
}
}
See Spring AMQP
Reference — Annotation-Driven Listener Endpoints for the full @RabbitListener/@QueueBinding/@RabbitHandler
attribute reference, and
Spring AMQP Reference — Handling Exceptions for errorHandler/returnExceptions in full.
Choosing a listener container
Spring AMQP ships two listener container implementations, selected per factory bean (or globally through
spring.rabbitmq.listener.type, simple by default or direct):
SimpleMessageListenerContainer (SMLC) |
|
|---|---|
|
Threading model |
A dedicated internal thread per consumer, fed from the RabbitMQ client’s own connection thread through an internal queue — one extra hand-off per delivery. |
The listener runs directly on the RabbitMQ client’s own consumer thread — no internal queue, no extra hand-off. |
Concurrency knob |
|
|
Adding/removing queues at runtime |
Restarts the whole set of consumer threads for the container. |
Only the affected queue’s consumer is added or cancelled — the others are unaffected. |
Batching |
|
No |
Container factory bean |
|
|
SMLC remains the default and the better fit when transactional batching or maxConcurrentConsumers
auto-scaling matters; DMLC is the better fit for a service whose set of consumed queues changes at runtime
(multi-tenant queue-per-tenant patterns, for example) or that wants to avoid the extra internal thread hand-off
per delivery.
spring:
rabbitmq:
listener:
type: direct
direct:
consumers-per-queue: 3
acknowledge-mode: manual
simple:
concurrency: 2
max-concurrency: 8
acknowledge-mode: manual
@Bean
public DirectRabbitListenerContainerFactory quorumContainerFactory(ConnectionFactory connectionFactory,
MessageConverter messageConverter) {
DirectRabbitListenerContainerFactory factory = new DirectRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConsumersPerQueue(3);
return factory;
}
See Spring AMQP
Reference — Choosing a Container for the complete decision guidance and
Spring AMQP Reference — Listener
Concurrency for the auto-scaling algorithm and the concurrency range-string syntax in full.
Message converters and type mapping
The default MessageConverter is SimpleMessageConverter (Java serialization for a Serializable payload, or a
raw byte[]/String pass-through) — essentially never what a real service wants. Registering a
JacksonJsonMessageConverter bean is picked up automatically by both the auto-configured RabbitTemplate and
the default listener container factory:
@Bean
public MessageConverter messageConverter() {
return new JacksonJsonMessageConverter();
}
Spring AMQP 4.0 deprecated the Jackson 2-based AbstractJackson2MessageConverter family (and
Jackson2JavaTypeMapper) for removal in favor of Jackson 3-based replacements, and 4.1 ships only the Jackson
3-based types — JacksonJsonMessageConverter and DefaultJacksonJavaTypeMapper (DefaultClassMapper) — rather than the Jackson2JsonMessageConverter still found in older Spring Boot examples.
On send, the converter stamps the payload’s fully-qualified class name into the TypeId message header (plus
ContentTypeId/KeyTypeId for a Map’s value/key types); on receive, it reads that same header back to
pick the target Java type, which only works when publisher and consumer share the same class on their respective
classpaths. `DefaultClassMapper#setIdClassMapping decouples the two sides by mapping an arbitrary string id to a
local type instead, and trustedPackages is the deserialization allow-list a converter needs before it will
instantiate anything outside java.util/java.lang by default — both matter as soon as publisher and consumer
are different services rather than the same codebase:
@Bean
public MessageConverter messageConverter() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
DefaultClassMapper classMapper = new DefaultClassMapper();
classMapper.setTrustedPackages("com.example.orders.events");
classMapper.setIdClassMapping(Map.of(
"order-created", OrderCreated.class,
"order-cancelled", OrderCancelled.class));
converter.setClassMapper(classMapper);
return converter;
}
A method parameter’s own declared type is used by default in preference to the inbound TypeId header
(TypePrecedence.INFERRED, the default); setting TypePrecedence.TYPE_ID on the converter instead trusts the
sender’s header over the listener method’s own signature, relevant mainly to the @RabbitHandler multi-method
dispatch above.
See Spring AMQP Reference — Message
Conversion for the complete converter hierarchy (including JacksonXmlMessageConverter and content-type-aware
conversion), and
Spring Boot Reference — AMQP for how a
MessageConverter bean is picked up by the auto-configured template and default container factory without
further wiring.
Acknowledgement, retry and dead lettering
RabbitMQ Reliability, Dead-Lettering and
TTL already covers dead-letter exchanges, the four dead-letter reasons and TTL/queue-length-driven dead-lettering
in full at the protocol level — a queue declared with .deadLetterExchange(…) on QueueBuilder above already
gets all of that. What is specific to the Spring layer is how a listener, rather than the broker, decides a
message should not be retried and should go straight to dead-lettering: throwing
AmqpRejectAndDontRequeueException from a listener method does exactly that, unconditionally.
A RetryInterceptorBuilder-built interceptor sits in front of that broker-level dead-lettering, retrying a
failed delivery a bounded number of times, in-process, before it ever counts as rejected:
@Bean
public RetryOperationsInterceptor listenerRetryInterceptor(RabbitTemplate rabbitTemplate) {
return RetryInterceptorBuilder.stateless()
.maxRetries(3)
.backOffOptions(1_000, 2.0, 10_000) // initialInterval, multiplier, maxInterval
.recoverer(new RepublishMessageRecoverer(rabbitTemplate, "orders.dlx", "orders.failed"))
.build();
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory, MessageConverter messageConverter,
RetryOperationsInterceptor listenerRetryInterceptor) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
factory.setAdviceChain(listenerRetryInterceptor);
return factory;
}
Stateless retry (RetryInterceptorBuilder.stateless()) keeps the failing delivery on the same thread and
retries it in a loop, appropriate when there is no surrounding transaction, or one is started inside the retry
callback itself. Stateful retry (.stateful()) is for the opposite case — a transaction already started
higher up the call stack, where the delivery has to be returned to the broker and redelivered for each attempt
instead of looped locally — and needs a way to recognize the same delivery across those redeliveries, normally
the message’s MessageId property (createMessageIds on the converter, or a custom MessageKeyGenerator).
RepublishMessageRecoverer above is what actually runs once retries are exhausted — it republishes the message,
with the failure’s exception message and original exchange/routing key added as headers, to wherever it’s told
(here, straight to the same dead-letter exchange the queue’s own x-dead-letter-exchange would have used) and
then acks the original delivery, which is what keeps the broker’s own dead-lettering from also firing on the
same message. RejectAndDontRequeueRecoverer, the default recoverer if none is set, instead just logs a WARN
and lets the exhausted delivery fall through to AmqpRejectAndDontRequeueException semantics, relying on the
queue’s own dead-letter configuration (or plain discard, if none is configured) to take it from there.
This is the Spring-side layer to reach for on top of, not instead of, the wait-queue retry topology RabbitMQ Reliability, Dead-Lettering and TTL builds from TTL and a DLX: the interceptor above retries in-process without a delay, while a wait-queue topology (or, on RabbitMQ 4.3 quorum queues, the broker-native delayed-retry feature that page also covers) adds a real, broker-held delay between attempts.
See
Spring AMQP Reference — Resilience: Recovering from Errors and Broker Failures for the complete
RetryInterceptorBuilder/MessageRecoverer API, including ImmediateRequeueAmqpException (the opposite of
AmqpRejectAndDontRequeueException) and batch-listener retry caveats.
Request/reply: RPC, direct reply-to and @SendTo
AmqpTemplate#convertSendAndReceive is the Spring equivalent of the correlation-id/reply-to RPC pattern
Consuming from RabbitMQ covers with direct reply-to at the AMQP
level — it publishes the request, blocks (or, with AsyncRabbitTemplate, returns a CompletableFuture) up to
replyTimeout (spring.rabbitmq.template.reply-timeout, 5 seconds by default) waiting for a correlated reply,
and returns null on timeout:
@Service
public class PriceQuoteClient {
private final RabbitTemplate rabbitTemplate;
public PriceQuoteClient(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public Quote requestQuote(QuoteRequest request) {
Quote quote = (Quote) rabbitTemplate.convertSendAndReceive("rpc", "quotes.request", request);
if (quote == null) {
throw new QuoteTimeoutException(request);
}
return quote;
}
}
useDirectReplyToContainer defaults to true since Spring AMQP 2.0, meaning RabbitTemplate uses RabbitMQ’s
amq.rabbitmq.reply-to pseudo-queue automatically — exactly the mechanism
Consuming from RabbitMQ describes — with no per-request queue
declared or torn down, and no reply-listener bean to configure. A fixed reply queue is still occasionally
useful (older brokers without direct reply-to support, or a reply pattern that must survive the requesting
process restarting) and is configured with a named Queue bean plus setReplyAddress(…) on the template and a
listener container that feeds replies back into the template itself:
@Bean
public Queue repliesQueue() {
return new Queue("orders.replies");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, Queue repliesQueue) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setReplyAddress(repliesQueue.getName());
template.setReplyTimeout(10_000);
return template;
}
@Bean
public SimpleMessageListenerContainer replyListenerContainer(ConnectionFactory connectionFactory,
Queue repliesQueue, RabbitTemplate rabbitTemplate) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(repliesQueue);
container.setMessageListener(rabbitTemplate); // RabbitTemplate itself implements MessageListener here
return container;
}
On the responding side, @SendTo turns an ordinary @RabbitListener method’s return value into the reply,
without the handler ever touching replyTo/correlationId itself — the framework copies the correlation
metadata from the inbound message onto the outbound reply automatically:
@RabbitListener(queues = "quotes.request")
@SendTo("quotes.reply")
public Quote quote(QuoteRequest request) {
return priceQuoteService.quote(request);
}
See Spring AMQP Reference — Request/Reply
Messaging for the complete sendAndReceive/convertSendAndReceive/AsyncRabbitTemplate API and fixed-reply-
queue configuration, and
Spring
AMQP Reference — Reply Management for @SendTo and ReplyPostProcessor in full.
Transactions
RabbitTransactionManager is a PlatformTransactionManager implementation for RabbitMQ, used the same way any
other Spring transaction manager is — @Transactional, or a TransactionTemplate — against a single
ConnectionFactory. It does not provide XA transactions across RabbitMQ and a database in the same unit of work,
only a local, RabbitMQ-only transaction (channel.txCommit()/txRollback() under the hood).
@Bean
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
return new RabbitTransactionManager(connectionFactory);
}
Transactions and publisher confirms are not meant to be combined — confirms already give a publisher a
per-message acknowledgement that costs far less than a full broker-side transaction commit, so
Publishing to RabbitMQ's confirm strategies remain the better
default for publisher reliability; reach for RabbitTransactionManager where a listener’s processing genuinely
needs to be atomic with its own re-publishing of other messages, not as a substitute for confirms.
See Spring AMQP Reference — Transactions
for the complete transaction-synchronization behavior, including how a @RabbitListener container’s own
channelTransacted setting interacts with a surrounding @Transactional boundary.
The delayed-message exchange plugin
RabbitMQ Reliability, Dead-Lettering and
TTL already flags the community delayed-message exchange plugin as unmaintained on current RabbitMQ releases, in
favor of RabbitMQ 4.3’s native delayed quorum-queue retries. Where the plugin is available, Spring AMQP’s support
for it is a single boolean on the exchange bean — RabbitAdmin uses it to declare the exchange with type
x-delayed-message and an x-delayed-type argument matching the exchange’s own type:
@Bean
public TopicExchange delayedOrdersExchange() {
TopicExchange exchange = new TopicExchange("orders.delayed");
exchange.setDelayed(true);
return exchange;
}
The per-message delay itself is the x-delay header, in milliseconds, set through MessageProperties#setDelay
or a MessagePostProcessor:
rabbitTemplate.convertAndSend("orders.delayed", "orders.eu.created", order,
message -> {
message.getMessageProperties().setDelay(15_000);
return message;
});
See Spring AMQP Reference — Delayed Message Exchange for this support in full, including getReceivedDelay() for reading the delay actually
applied to an inbound message.
Multiple brokers
Talking to more than one broker (or the same cluster split across two vhosts, or a producer/consumer split to
avoid the blocked-connection deadlock in the next section) from one Spring Boot application means excluding
RabbitAutoConfiguration and wiring ConnectionFactory/RabbitAdmin/container factory beans by hand — Boot’s
own auto-configuration only ever wires one of each. A SimpleRoutingConnectionFactory picks a target connection
factory per operation by a routing key, and MultiRabbitListenerAnnotationBeanPostProcessor lets @RabbitListener
methods each target a different RabbitListenerContainerFactory/broker instead of only the single default one:
@SpringBootApplication(exclude = RabbitAutoConfiguration.class)
public class Application {
@Bean
CachingConnectionFactory ordersConnectionFactory() {
return new CachingConnectionFactory("orders-broker");
}
@Bean
CachingConnectionFactory auditConnectionFactory() {
return new CachingConnectionFactory("audit-broker");
}
@Bean
SimpleRoutingConnectionFactory routingConnectionFactory(CachingConnectionFactory ordersConnectionFactory,
CachingConnectionFactory auditConnectionFactory) {
SimpleRoutingConnectionFactory routingFactory = new SimpleRoutingConnectionFactory();
routingFactory.setDefaultTargetConnectionFactory(ordersConnectionFactory);
routingFactory.setTargetConnectionFactories(
Map.of("orders", ordersConnectionFactory, "audit", auditConnectionFactory));
return routingFactory;
}
@Bean("orders-admin")
RabbitAdmin ordersAdmin(CachingConnectionFactory ordersConnectionFactory) {
return new RabbitAdmin(ordersConnectionFactory);
}
@Bean("audit-admin")
RabbitAdmin auditAdmin(CachingConnectionFactory auditConnectionFactory) {
return new RabbitAdmin(auditConnectionFactory);
}
}
See Spring AMQP Reference — Multiple Broker
(or Cluster) Support for the complete routing-connection-factory and @RabbitListener-per-broker setup,
including how auto-declared queues get associated with the right target broker automatically.
Handling a blocked connection
RabbitMQ blocks a connection — stops reading from it — once a memory or disk resource alarm trips, the same
flow-control mechanism Publishing to RabbitMQ covers at the AMQP
level. Spring AMQP surfaces that as a ConnectionBlockedEvent/ConnectionUnblockedEvent ApplicationEvent pair
published by the connection factory, or a com.rabbitmq.client.BlockedListener registered directly on the
underlying Connection, either of which lets application code react (pause background publishing, raise an
alert, shed load) instead of a producer thread simply stalling with no explanation:
@Component
public class BlockedConnectionMonitor {
@EventListener
public void onBlocked(ConnectionBlockedEvent event) {
log.error("RabbitMQ connection blocked: {}", event.getReason());
}
@EventListener
public void onUnblocked(ConnectionUnblockedEvent event) {
log.info("RabbitMQ connection unblocked");
}
}
A single CachingConnectionFactory shared between producers and consumers — the default under plain Boot
auto-configuration — has a specific failure mode worth calling out: if producers are the ones triggering the
resource alarm and consumers on the same connection are what would free up the broker again, a blocked
connection can deadlock the whole application. RabbitTemplate#setUsePublisherConnection(true) has the template
use a second, dedicated connection for publishing (transparently derived from the main connection factory)
specifically to avoid that deadlock, without standing up a fully separate ConnectionFactory bean by hand.
See Spring AMQP Reference — Connection and Resource Management for the complete blocked-connection event contract and the "Using a Separate Connection" section this deadlock mitigation is documented under.
Streams: RabbitStreamTemplate and the AMQP 1.0 path
RabbitMQ Queue Types introduces RabbitMQ streams; Spring AMQP
covers them from a separate module and API family from everything above, spring-rabbit-stream:
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-stream</artifactId>
</dependency>
RabbitStreamTemplate wraps the same Environment/Producer the stream protocol client uses directly on
Publishing to RabbitMQ, returning a CompletableFuture<Boolean>
per send rather than blocking, and Boot auto-configures both the Environment bean and, when
spring.rabbitmq.listener.type=stream, a StreamRabbitListenerContainerFactory for @RabbitListener:
spring:
rabbitmq:
stream:
name: orders-stream
listener:
type: stream
stream:
native-listener: true # listener receives com.rabbitmq.stream.Message directly, not converted
@Bean
public Queue ordersStream() {
return QueueBuilder.durable("orders-stream").stream().build();
}
@Component
public class OrderStreamPublisher {
private final RabbitStreamTemplate streamTemplate;
public OrderStreamPublisher(RabbitStreamTemplate streamTemplate) {
this.streamTemplate = streamTemplate;
}
public void publish(Order order) {
streamTemplate.convertAndSend(order);
}
}
@Component
public class OrderStreamListener {
@RabbitListener(queues = "orders-stream", containerFactory = "rabbitListenerContainerFactory")
public void onOrder(Order order) {
process(order);
}
}
See Spring AMQP Reference — Using the RabbitMQ Stream
Plugin for the complete RabbitStreamTemplate/StreamListenerContainer API, offset tracking (the Spring
equivalent of Consuming from RabbitMQ's stream offset-tracking
section) and super-stream support.
The AMQP 1.0 path: spring-rabbitmq-client
RabbitMQ and AMQP 1.0 covers the protocol this module speaks — sessions and links instead of channels, credit-based flow control instead of prefetch, settlement outcomes
instead of ack/nack. spring-rabbitmq-client (new in Spring AMQP 4.0, RabbitMQ-specific — not the same module
as the broker-agnostic spring-amqp-client AMQP 1.0 support new in 4.1) mirrors the classic spring-rabbit API
one level down, with every send returning a CompletableFuture instead of blocking:
| Concept | Classic (spring-rabbit, AMQP 0-9-1) |
AMQP 1.0 (spring-rabbitmq-client) |
|---|---|---|
Connection factory |
|
|
Template |
|
|
Admin |
|
|
Listener container |
|
|
@Bean
Environment environment() {
return new AmqpEnvironmentBuilder().build();
}
@Bean
AmqpConnectionFactory amqpConnectionFactory(Environment environment) {
return new SingleAmqpConnectionFactory(environment);
}
@Bean
RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory amqpConnectionFactory) {
return new RabbitAmqpAdmin(amqpConnectionFactory);
}
@Bean
RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory amqpConnectionFactory) {
return new RabbitAmqpTemplate(amqpConnectionFactory);
}
@Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory amqpConnectionFactory) {
return new RabbitAmqpListenerContainerFactory(amqpConnectionFactory);
}
@RabbitListener on this path accepts AmqpAcknowledgment (settlement — acknowledge(),
acknowledge(Status.REQUEUE), and so on) or the native Consumer.Context directly, and its ackMode attribute
takes an AcknowledgeMode the same way the container-level property does on the classic path:
@RabbitListener(queues = "orders", ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}")
public void onOrder(Order order, AmqpAcknowledgment acknowledgment) {
try {
process(order);
acknowledgment.acknowledge(); // settlement outcome: accepted
} catch (RecoverableException e) {
acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); // settlement outcome: released
}
}
See Spring AMQP Reference — RabbitMQ AMQP
1.0 Support for the complete Environment/AmqpConnectionFactory/RabbitAmqpTemplate/
RabbitAmqpListenerContainerFactory API, and
Spring AMQP Reference — Generic AMQP 1.0
Support for the newer, broker-agnostic spring-amqp-client/@AmqpListener module this page does not otherwise
cover, since this section’s scope is RabbitMQ specifically.