Skip to content

@JmsListener in Spring Boot: Consuming JMS Messages Made Easy

Purpose

Spring’s @JmsListener annotation makes consuming JMS messages incredibly simple. Add this annotation to a method, specify your queue destination, and Spring creates a message listener container that automatically polls the queue and invokes your method for each message. No manual JMS API code needed—Spring handles connection management, polling, and message deserialization for you.

How @JmsListener Works

When you add @JmsListener to a method, Spring Boot automatically:

  1. Creates a message listener container
  2. Starts polling the specified JMS queue
  3. Deserializes incoming messages using your configured MessageConverter
  4. Passes the deserialized POJO directly to your method

Let me show you the simplest example.

Basic @JmsListener Setup

Here’s a minimal example that listens for Article objects on a queue:

ArticleListener.java
@Component
public class ArticleListener {
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
System.out.println("Received article: " + article.getTitle());
// Process the article...
}
}

That’s it! Spring automatically polls article.queue, deserializes JSON messages into Article objects, and passes them to your method.

Message Record Type

I recommend using Java Records for message types—they’re immutable and perfect for messaging:

Article.java
public record Article(
String id,
String title,
String content,
String author,
LocalDateTime publishedAt
) {}

Spring’s MappingJackson2MessageConverter handles the JSON-to-Record conversion automatically.

Listening to Multiple Queues

You can have multiple listener methods in the same class, each listening to a different queue:

MultiQueueListener.java
@Component
public class OrderListener {
private final OrderService orderService;
private final NotificationService notificationService;
public OrderListener(OrderService orderService,
NotificationService notificationService) {
this.orderService = orderService;
this.notificationService = notificationService;
}
@JmsListener(destination = "order.created.queue")
public void handleOrderCreated(Order order) {
orderService.processNewOrder(order);
}
@JmsListener(destination = "order.cancelled.queue")
public void handleOrderCancelled(OrderCancellation cancellation) {
orderService.cancelOrder(cancellation.getOrderId());
notificationService.notifyCustomer(
cancellation.getCustomerId(),
"Your order has been cancelled"
);
}
@JmsListener(destination = "payment.completed.queue")
public void handlePaymentCompleted(Payment payment) {
orderService.markOrderAsPaid(payment.getOrderId());
}
}

Each method can receive a different message type. Spring uses the method parameter type to determine how to deserialize the message.

Required Configuration

For @JmsListener to work, you need a MessageConverter bean to handle JSON deserialization:

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

Spring Boot auto-configures a JmsListenerContainerFactory, but you can customize it if needed:

CustomJmsConfig.java
@Configuration
public class CustomJmsConfig {
@Bean
public JmsListenerContainerFactory<?> myFactory(
ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory =
new DefaultJmsListenerContainerFactory();
configurer.configure(factory, connectionFactory);
factory.setErrorHandler(t -> {
System.err.println("Error in listener: " + t.getMessage());
});
return factory;
}
}

Then reference it in your listener:

CustomFactoryListener.java
@JmsListener(destination = "important.queue", containerFactory = "myFactory")
public void processImportantMessage(Message message) {
// Uses custom factory with error handling
}

Error Handling in Listeners

Error handling is critical in message listeners. Unhandled exceptions cause messages to be redelivered, which can create infinite retry loops.

Basic Error Handling

SafeArticleListener.java
@Component
public class SafeArticleListener {
private final ArticleService articleService;
public SafeArticleListener(ArticleService articleService) {
this.articleService = articleService;
}
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
try {
articleService.save(article);
} catch (DataAccessException e) {
// Log and rethrow to trigger redelivery
System.err.println("Database error processing article: " +
article.getId() + ", error: " + e.getMessage());
throw new RuntimeException("Failed to process article", e);
} catch (ValidationException e) {
// Log and discard - don't rethrow
System.err.println("Invalid article: " + article.getId());
// Message will be acknowledged and not redelivered
}
}
}

Using @Retryable for Transient Failures

For transient failures like network issues, use Spring Retry:

RetryableListener.java
@Component
public class RetryableListener {
@Retryable(
retryFor = {TransientDataAccessException.class, JmsException.class},
maxAttempts = 3,
backoff = @Backoff(delay = 1000, multiplier = 2)
)
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
// This will retry up to 3 times with exponential backoff
articleService.save(article);
}
@Recover
public void recover(RuntimeException e, Article article) {
// Called after all retries fail
System.err.println("Failed to process article after retries: " +
article.getId());
// Send to dead letter queue or log for manual intervention
}
}

Common Mistakes to Avoid

Mistake 1: Not Registering a MessageConverter

MissingConverter.java
// WRONG: No MessageConverter configured
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
// This will fail with MessageConversionException
}

Always configure a MessageConverter:

CorrectConverter.java
@Configuration
public class JmsConfig {
@Bean
public MessageConverter jacksonJmsMessageConverter() {
return new MappingJackson2MessageConverter();
}
}

Mistake 2: Wrong Message Type in Parameter

WrongMessageType.java
// WRONG: Queue sends Article, but method expects String
@JmsListener(destination = "article.queue")
public void processArticle(String message) { // Wrong type!
// Will cause MessageConversionException
}

Match your method parameter to the message type:

CorrectMessageType.java
// CORRECT: Match parameter type to message
@JmsListener(destination = "article.queue")
public void processArticle(Article article) { // Correct type
articleService.save(article);
}

Mistake 3: Blocking Listener Thread

BlockingListener.java
// WRONG: Long-running operation blocks the listener thread
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
// This blocks the listener thread for up to 30 seconds!
processHeavyComputation(article); // Blocking call
callExternalApi(article); // Blocking call
}

Instead, delegate to an async service:

AsyncListener.java
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
// Delegate to async service - listener thread returns immediately
articleProcessingService.processAsync(article);
}
@Service
public class ArticleProcessingService {
@Async
public void processAsync(Article article) {
processHeavyComputation(article);
callExternalApi(article);
}
}

Mistake 4: Not Handling Exceptions

NoErrorHandling.java
// WRONG: Unhandled exception causes message redelivery
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
articleService.save(article); // If this throws, message is redelivered
}

Always handle exceptions:

ProperErrorHandling.java
@JmsListener(destination = "article.queue")
public void processArticle(Article article) {
try {
articleService.save(article);
} catch (DataAccessException e) {
log.error("Failed to save article: {}", article.getId(), e);
throw new RuntimeException("Trigger redelivery", e);
} catch (ValidationException e) {
log.warn("Invalid article data: {}", article.getId(), e);
// Don't rethrow - acknowledge and discard invalid messages
}
}

Summary

In this post, I showed you how @JmsListener simplifies JMS message consumption in Spring Boot:

  • Automatic polling: Spring creates a listener container that polls your queue automatically
  • POJO deserialization: Messages are converted to Java objects using your MessageConverter
  • Simple API: Just add the annotation and implement your method—no JMS boilerplate
  • Multiple queues: One class can listen to multiple queues with different message types
  • Error handling: Proper exception handling prevents infinite retry loops

The key takeaway is that @JmsListener eliminates all the JMS boilerplate code. You focus on your business logic, and Spring handles the messaging infrastructure.

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