Spring Boot: Other Messaging Options
|
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. |
Kafka and RabbitMQ cover this section’s two main brokers in depth. This page rounds out the rest of what a Spring Boot 4.1.x application (Spring Framework 7.0.x) reaches for when a message needs to move somewhere — in-process, over JMS, through Spring Integration, over WebSocket, or to Apache Pulsar — and says, briefly, what is deliberately left out.
In-process events: ApplicationEventPublisher and @EventListener
Before reaching for a broker at all, a Spring application already has a messaging mechanism built into the
ApplicationContext: publish an event with ApplicationEventPublisher.publishEvent(Object), and any bean with a
matching @EventListener method receives it, synchronously, on the publishing thread, by default. This is the
right tool for decoupling two parts of the same process — an order service that should not know which other
components care that an order was placed — without any of it leaving the JVM.
public record OrderPlacedEvent(String orderId) {
}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
publisher.publishEvent(new OrderPlacedEvent(order.id()));
}
}
A plain @EventListener fires while placeOrder is still inside its transaction, which is often not what a
listener that sends a notification or calls another service wants — it would run even if the transaction later
rolled back. @TransactionalEventListener binds the listener to a TransactionPhase instead: BEFORE_COMMIT,
AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION for either outcome. Combined with @Async,
the listener also moves off the caller’s thread, so placeOrder returns without waiting for it:
@Component
public class OrderPlacedNotifier {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderPlaced(OrderPlacedEvent event) {
notificationClient.notifyOrderPlaced(event.orderId());
}
}
@Async here requires @EnableAsync and a configured TaskExecutor, exactly as for any other asynchronous
method. See
Spring
Framework Documentation — Standard and Custom Events for ApplicationEventPublisher, @EventListener and
@Async listeners, and
Spring Framework
Documentation — Transaction-bound Events for @TransactionalEventListener and TransactionPhase.
Spring Modulith: externalizing application events to Kafka or RabbitMQ
Application events stop at the process boundary — a second instance of the same service, or a different service
entirely, never sees them. Spring Modulith’s event externalization bridges exactly that gap: the same
ApplicationEventPublisher.publishEvent() call above, annotated once, also lands on a broker topic or exchange,
with no separate publishing code path to keep in sync.
An event opts in with @Externalized, whose value is a routing target and, after ::, an optional routing key — either can be a SpEL expression evaluated against the event instance:
@Externalized("orders.order-placed::#{#this.orderId()}")
public record OrderPlacedEvent(String orderId) {
}
Which broker the routing target resolves against depends purely on which Spring Modulith events artifact is on
the classpath: spring-modulith-events-kafka routes it as a Kafka topic and message key (via Spring Kafka, so a
KafkaTemplate still needs its usual connection properties), and spring-modulith-events-amqp routes it as an
AMQP exchange/routing key (via Spring AMQP, with an explicit spring-rabbit dependency still required). Both
depend on spring-modulith-events-api, which also exposes EventExternalizationConfiguration for programmatic
control — selecting which events externalize, mapping an event to a different payload, or adding headers — when annotating every event class is not the right fit:
@Configuration
public class EventExternalizationConfig {
@Bean
EventExternalizationConfiguration eventExternalizationConfiguration() {
return EventExternalizationConfiguration.externalizing()
.select(EventExternalizationConfiguration.annotatedAsExternalized())
.routeKey(OrderPlacedEvent.class, OrderPlacedEvent::orderId)
.build();
}
}
This only externalizes what a listener inside the same module already reacted to in-process — it is not a
replacement for Kafka or
RabbitMQ producer code, it is a way to avoid
hand-writing that producer code for events a module already publishes internally. See
Spring Modulith Documentation — Application Events
for the full @Externalized target syntax, the JMS and Spring Messaging externalization artifacts this page does
not cover, and the transactional-outbox modes for relational databases — see
The Transactional Outbox Pattern for the pattern itself.
JMS / Jakarta Messaging with ActiveMQ Artemis
Where Kafka and RabbitMQ (AMQP) are covered in their own sections of this site, spring-boot-starter-artemis
brings in Jakarta Messaging (JMS) against Apache ActiveMQ Artemis, Spring Boot’s supported JMS broker. JmsTemplate
is auto-configured from spring.jms./spring.artemis. properties — spring.artemis.mode chooses native
(connect to a standalone broker via spring.artemis.broker-url) or embedded (start an in-process broker, useful
for tests) — and sending is a single call:
@Component
public class OrderJmsSender {
private final JmsTemplate jmsTemplate;
public OrderJmsSender(JmsTemplate jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
public void send(Order order) {
jmsTemplate.convertAndSend("orders.queue", order);
}
}
@JmsListener (enabled by the auto-configured JmsListenerContainerFactory, no @EnableJms needed in a Boot
application) consumes it:
@Component
public class OrderJmsListener {
@JmsListener(destination = "orders.queue")
public void onOrder(Order order) {
process(order);
}
}
JMS destinations are queues by default; a destination becomes a topic by setting pubSubDomain to true on the
listener container factory (or the JmsTemplate used to send), which is the JMS analogue of choosing point-to-point
versus publish-subscribe delivery. For request/reply, JmsTemplate.convertSendAndReceive() sends and blocks for a
correlated reply, and @JmsListener can answer inline with @SendTo(destination) on the handler method, letting
the container populate the sender’s JMSReplyTo destination without extra wiring.
RabbitMQ ships its own JMS client (com.rabbitmq.jms, layered over AMQP 0-9-1, with a broker-side plugin needed
for topic destinations) for applications that want the JMS API against a RabbitMQ broker specifically — it is a
separate integration from the Artemis-based JMS support above and not interchangeable with it at the wire level,
so pick one JMS provider per destination, not a mix. See
Spring Boot Documentation — JMS for the
spring-boot-starter-artemis auto-configuration, spring.artemis./spring.jms. properties and connection
pooling, and
Spring Framework Documentation — JMS
(Java Message Service) for JmsTemplate, @JmsListener/@EnableJms, message conversion and
convertSendAndReceive().
Spring Integration: channel adapters for AMQP and Kafka
Spring Integration turns "send this to AMQP" or "consume from this Kafka topic" into a MessageChannel endpoint
in a Java DSL IntegrationFlow, so it composes with transformers, routers, splitters and aggregators instead of
being one more standalone client to wire by hand. Amqp.outboundAdapter()/Amqp.inboundAdapter() and
Kafka.outboundChannelAdapter()/Kafka.messageDrivenChannelAdapter() are the entry points for each broker:
@Bean
public IntegrationFlow orderEventsToRabbit(RabbitTemplate rabbitTemplate) {
return IntegrationFlow.from("orderEvents")
.handle(Amqp.outboundAdapter(rabbitTemplate).routingKey("orders.placed"))
.get();
}
@Bean
public IntegrationFlow orderEventsToKafka(ProducerFactory<String, Order> producerFactory) {
return IntegrationFlow.from("orderEvents")
.handle(Kafka.outboundChannelAdapter(producerFactory).topic("orders"))
.get();
}
The same families of adapters — Amqp.inboundAdapter() and Kafka.messageDrivenChannelAdapter() for consuming,
plus their gateway counterparts for request/reply — are exactly what
Spring Batch Integration uses to wire remote chunking and remote partitioning
across a message broker; that page covers the batch-specific configuration (@EnableBatchIntegration,
ChunkMessageChannelItemWriter, MessageChannelPartitionHandler) in depth and is not repeated here. See
Spring Integration Documentation — AMQP Support
and Spring Integration Documentation — Apache
Kafka Support for the full adapter and gateway catalogue, including the pollable Kafka.inboundChannelAdapter()
variant and Kafka-backed `MessageChannel`s.
WebSocket messaging with STOMP
@EnableWebSocketMessageBroker turns Spring MVC into a STOMP-over-WebSocket endpoint: clients subscribe to
destinations and a @Controller handles messages sent to /app/*, exactly like an HTTP controller handles
requests. Where subscriptions get broadcast is a separate choice — the *simple broker keeps subscriptions and
messages in memory inside the application itself, fine for a single instance or local development:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue");
}
}
Swapping enableSimpleBroker for enableStompBrokerRelay hands broadcasting to an external, full STOMP broker
instead — RabbitMQ, with its STOMP plugin enabled, is a common choice, since it is then the same broker already
running for AMQP messaging elsewhere in the
application, and it is what makes multi-instance fan-out and acks/receipts work, which the simple broker does not
support:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("rabbitmq")
.setRelayPort(61613)
.setClientLogin("app")
.setClientPasscode("secret");
}
Server-side code publishes to either broker the same way, through an injected SimpMessagingTemplate, without
knowing which one is behind it. See
Spring Framework Documentation — Enabling STOMP over WebSocket for @EnableWebSocketMessageBroker and endpoint/broker registration, and
Spring Framework
Documentation — STOMP Broker Relay for enableStompBrokerRelay() and its RabbitMQ-relay configuration.
Apache Pulsar
spring-boot-starter-pulsar auto-configures a PulsarClient against spring.pulsar.client.service-url and,
from it, a PulsarTemplate for producing and a listener container factory for @PulsarListener:
@Component
public class OrderPulsarProducer {
private final PulsarTemplate<Order> pulsarTemplate;
public OrderPulsarProducer(PulsarTemplate<Order> pulsarTemplate) {
this.pulsarTemplate = pulsarTemplate;
}
public void send(Order order) {
pulsarTemplate.send("orders", order);
}
}
@Component
public class OrderPulsarListener {
@PulsarListener(topics = "orders", subscriptionName = "orders-subscription")
public void onOrder(Order order) {
process(order);
}
}
spring.pulsar.producer. and spring.pulsar.consumer./spring.pulsar.listener.* tune producer batching/caching
and consumer/container behavior respectively, and spring.pulsar.admin.service-url configures the auto-configured
PulsarAdministration client for topic/subscription management. @PulsarReader is the manual-cursor counterpart
to @PulsarListener for applications that need to control message acknowledgement and positioning themselves
rather than through a managed subscription. See
Spring Boot Documentation — Apache Pulsar for
the starter’s auto-configuration and property surface, and
Spring for Apache Pulsar Documentation for PulsarTemplate,
@PulsarListener/@PulsarReader and the underlying client/producer/consumer factories.
What this page defers
Redis pub/sub and Redis Streams are both viable Spring Boot messaging options in their own right, but they are deliberately out of scope here and covered instead under Redis Reference: Streams & Pub/Sub and, for the Spring Data Redis repository/template side, Spring Data Redis & Custom Queries, rather than being covered briefly and incompletely on this one.