@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:
- Creates a message listener container
- Starts polling the specified JMS queue
- Deserializes incoming messages using your configured
MessageConverter - 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:
@Componentpublic 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:
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:
@Componentpublic 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:
@Configurationpublic 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:
@Configurationpublic 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:
@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
@Componentpublic 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:
@Componentpublic 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
// WRONG: No MessageConverter configured@JmsListener(destination = "article.queue")public void processArticle(Article article) { // This will fail with MessageConversionException}Always configure a MessageConverter:
@Configurationpublic class JmsConfig { @Bean public MessageConverter jacksonJmsMessageConverter() { return new MappingJackson2MessageConverter(); }}Mistake 2: Wrong Message Type in Parameter
// 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:
// 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
// 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:
@JmsListener(destination = "article.queue")public void processArticle(Article article) { // Delegate to async service - listener thread returns immediately articleProcessingService.processAsync(article);}
@Servicepublic class ArticleProcessingService {
@Async public void processAsync(Article article) { processHeavyComputation(article); callExternalApi(article); }}Mistake 4: Not Handling Exceptions
// 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:
@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