Skip to content

How to Use Spring 7.0 JmsClient: The New Fluent JMS API

Purpose

This post demonstrates how to use Spring Framework 7.0’s new JmsClient fluent API for sending JMS messages.

When I worked with JMS in Spring applications, I always found JmsTemplate verbose. Sending a simple message required multiple lines of boilerplate code. I wished for something cleaner.

Spring 7.0 introduced JmsClient - a fluent API that makes JMS operations much simpler. Let me show you how to use it.

Environment

  • Spring Framework 7.0
  • Spring Boot 4.0
  • Java 21
  • ActiveMQ Artemis 2.38
  • Maven 3.9

The Problem with JmsTemplate

Before Spring 7.0, I used JmsTemplate to send JMS messages. Here’s what the code looked like:

MessageSender.java
@Service
public class MessageSender {
@Autowired
private JmsTemplate jmsTemplate;
public void sendMessage(String queueName, String message) {
jmsTemplate.send(queueName, session -> {
TextMessage textMessage = session.createTextMessage(message);
textMessage.setJMSCorrelationID(UUID.randomUUID().toString());
textMessage.setJMSTimestamp(System.currentTimeMillis());
return textMessage;
});
}
}

I found several problems with this approach:

  1. Verbose: Too much boilerplate for simple operations
  2. Callback hell: Nested lambdas for setting message properties
  3. Type confusion: Mix of JMS-specific types and Spring types
  4. QoS settings: Hard to configure quality of service settings inline

When I needed to set time-to-live or receive timeout, the code became even more complex.

The Solution: JmsClient

Spring 7.0 introduced JmsClient with a fluent API. Here’s the same operation:

MessageSender.java
@Service
public class MessageSender {
@Autowired
private JmsClient jmsClient;
public void sendMessage(String queueName, String message) {
jmsClient.destination(queueName)
.withTimeToLive(60000)
.send(message);
}
}

You can see the difference. The fluent API chains destination selection with message sending in a single expression. I found this much easier to read and maintain.

Setting Up ActiveMQ Artemis

First, I set up ActiveMQ Artemis as the JMS broker. I used Docker for local development:

docker-compose.yml
services:
artemis:
image: apache/activemq-artemis:2.38.0
ports:
- "61616:61616"
- "8161:8161"
environment:
ARTEMIS_USER: artemis
ARTEMIS_PASSWORD: artemis

Run it with:

Terminal window
docker-compose up -d

The Artemis console is available at http://localhost:8161 with username artemis and password artemis.

Spring Boot Configuration

I added the Artemis starter to my pom.xml:

pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>

Then I configured the connection in application.yml:

src/main/resources/application.yml
spring:
artemis:
mode: native
broker-url: tcp://localhost:61616
user: artemis
password: artemis

Spring Boot auto-configures both JmsTemplate and JmsClient beans when the starter is on the classpath.

Injecting JmsClient

I injected the auto-configured JmsClient bean:

OrderService.java
@Service
public class OrderService {
private final JmsClient jmsClient;
public OrderService(JmsClient jmsClient) {
this.jmsClient = jmsClient;
}
public void placeOrder(Order order) {
jmsClient.destination("orders.queue")
.send(order);
}
}

I used constructor injection here because it makes testing easier and clearly shows dependencies.

Sending Different Payload Types

JmsClient handles different payload types automatically. I tried sending a POJO:

OrderService.java
public record Order(
String orderId,
String customerId,
List<OrderItem> items,
BigDecimal totalAmount
) {}
@Service
public class OrderService {
private final JmsClient jmsClient;
public void sendOrder(Order order) {
// Send POJO directly - JmsClient converts to JSON
jmsClient.destination("orders.queue")
.withTimeToLive(300000) // 5 minutes
.send(order);
}
}

For POJO serialization to work, I added Jackson:

pom.xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

And registered a message converter:

JmsConfig.java
@Configuration
public class JmsConfig {
@Bean
public MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
}

Sending with Custom Headers

I often need to add custom headers to messages. With JmsClient, I can pass a headers map:

OrderService.java
public void sendOrderWithMetadata(Order order, String correlationId) {
Map<String, Object> headers = new HashMap<>();
headers.put("X-Correlation-ID", correlationId);
headers.put("X-Source", "order-service");
headers.put("X-Priority", "HIGH");
jmsClient.destination("orders.queue")
.send(order, headers);
}

Or use Spring’s MessageBuilder:

OrderService.java
import org.springframework.messaging.support.MessageBuilder;
public void sendOrderAsMessage(Order order, String correlationId) {
Message<Order> message = MessageBuilder
.withPayload(order)
.setHeader("X-Correlation-ID", correlationId)
.setHeader("X-Source", "order-service")
.build();
jmsClient.destination("orders.queue")
.send(message);
}

Receiving Messages

JmsClient also supports receiving messages with the same fluent style:

OrderProcessor.java
@Service
public class OrderProcessor {
private final JmsClient jmsClient;
public void processNextOrder() {
Optional<Order> order = jmsClient.destination("orders.queue")
.withReceiveTimeout(5000) // 5 seconds
.receive(Order.class);
order.ifPresent(this::handleOrder);
}
private void handleOrder(Order order) {
// Process the order
System.out.println("Processing order: " + order.orderId());
}
}

The receive() method returns an Optional containing the converted payload, or empty if no message was received within the timeout.

For receiving the full Spring Message object with headers:

OrderProcessor.java
public void processNextOrderWithHeaders() {
Optional<Message<?>> message = jmsClient.destination("orders.queue")
.withReceiveTimeout(5000)
.receive();
message.ifPresent(msg -> {
String correlationId = (String) msg.getHeaders().get("X-Correlation-ID");
Order order = (Order) msg.getPayload();
System.out.println("Correlation ID: " + correlationId);
System.out.println("Order: " + order.orderId());
});
}

QoS Settings

I found the quality of service settings easy to configure inline:

MessageSender.java
public void sendWithQos(String payload) {
jmsClient.destination("important.queue")
.withTimeToLive(86400000) // 24 hours
.withPriority(9) // High priority
.withDeliveryMode(DeliveryMode.PERSISTENT)
.send(payload);
}

These settings are applied to the operation without affecting other operations.

Common Mistakes I Made

Mistake 1: Forgetting Broker Configuration

When I first tried JmsClient, I got this error:

No qualifying bean of type 'org.springframework.jms.core.JmsClient' available

The fix was adding the Artemis starter dependency and configuring the broker URL in application.yml.

Mistake 2: No MessageConverter for POJOs

When I sent a POJO without a message converter, I got:

MessageConversionException: Cannot convert object of type [Order] to JMS message

I fixed it by registering the MappingJackson2MessageConverter bean.

Mistake 3: Assuming Full JmsTemplate Feature Parity

I expected JmsClient to have all features of JmsTemplate. But JmsClient is focused on common operations. For advanced scenarios like browsing queues or executing callbacks with JMS sessions, I still need JmsTemplate.

When to Use JmsClient vs JmsTemplate

I use JmsClient when:

  • Sending simple messages
  • Receiving with timeout
  • Need fluent, readable code
  • Working with POJOs and JSON

I still use JmsTemplate when:

  • Browsing queues
  • Need JMS Session callbacks
  • Using advanced JMS features
  • Working with existing codebase

Summary

In this post, I showed how to use Spring Framework 7.0’s new JmsClient fluent API for JMS operations. The key points are:

  • JmsClient provides a cleaner, chainable API compared to JmsTemplate
  • Spring Boot auto-configures JmsClient when JMS starter is on classpath
  • Use destination("queue").send(payload) for sending messages
  • Use destination("queue").receive(Type.class) for receiving messages
  • QoS settings like time-to-live and priority are configured inline
  • Register a MessageConverter for POJO serialization

I found JmsClient makes my JMS code more readable and maintainable. The fluent API design fits well with modern Java coding style.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments