Docs navigation
On this page

JMS

1 Introduction

This project includes integration between Micronaut and Java Messaging Service (JMS).

Supported implementations include Active MQ "Classic", Active MQ Artemis, and AWS SQS, and configuring other JMS providers is straightforward.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Breaking Changes

5.0.0

The way of configuring unsupported brokers has changed. See the Customizing Brokers section.

4.0.0

Removal of javax.jms namespace

Version 4 switches from the deprecated javax.jms namespace to the new Jakarta EE jakarta.jms namespace.

ActiveMQ Classic

The ActiveMQ Classic client has been updated to version 6 to support the new namespace.

ActiveMQ Artemis

The client dependency for ActiveMq Artemis has changed from org.apache.activemq:artemis-jms-client to org.apache.activemq:artemis-jakarta-client which uses the new namespace.

Deprecations

  • The annotation io.micronaut.jms.annotations.Queue removes the following attributes that were deprecated previously. This is to align the implementation with the JMS model and the messaging libraries' presumptions.

    • String concurrency() default "1-1"

    • String executor() default ""

  • The annotation io.micronaut.jms.annotations.Topic removes the following attributes that were deprecated previously. This is to align the implementation with the JMS model and the messaging libraries' presumptions.

    • String executor() default ""

  • The following classes in the io.micronaut.jms.listener package were deprecated previously are now removed.

    • ConcurrentMessageHandler

    • JMSListenerContainer

    • JMSListenerContainerFactory

4 JMS Quick Start

To add support for JMS to a project, add a dependency to your build, depending on the JMS implementation(s) you require:

implementation("io.micronaut.jms:micronaut-jms-activemq-classic")
implementation("io.micronaut.jms:micronaut-jms-activemq-artemis")
implementation("io.micronaut.jms:micronaut-jms-sqs")

Note that you can use multiple providers in a project.

Creating a JMS Producer with @JMSProducer

To create a JMS client that sends messages, define an interface annotated with JMSProducer, for example:

Note
In Python the producer is an abstract class (ABC) whose @abstractmethod methods have …​ bodies; the parameter binding annotations are applied with typing.Annotated.

At compile time Micronaut will produce an implementation of the interface. You can retrieve an instance of TextProducer either by looking up the bean from the ApplicationContext or with dependency injection:

TextProducer textProducer = applicationContext.getBean(TextProducer.class);
textProducer.send("quickstart");

Creating a JMS Consumer with @JMSListener

To listen to JMS messages you can use the @JMSListener annotation to define a message listener.

The following example will listen for messages published by the TextProducer in the previous section:

5 Configuration

5.1 Configuring ActiveMQ "Classic"

To use ActiveMQ "Classic", add the following properties to your application.yml

micronaut.jms.activemq.classic.enabled=true
micronaut.jms.activemq.classic.connection-string=tcp://host:port

and the following to your build.gradle or pom.xml

implementation("io.micronaut.jms:micronaut-jms-activemq-classic")

The JMS ConnectionFactory will be an instance of ActiveMQConnectionFactory. Only the broker URL is configurable in application.yml but further customization is possible with a BeanCreatedEventListener.

5.2 Configuring ActiveMQ Artemis

To use ActiveMQ Artemis, add the following properties to your application.yml

micronaut.jms.activemq.artemis.enabled=true
micronaut.jms.activemq.artemis.connection-string=tcp://host:port

and the following to your build.gradle or pom.xml

implementation("io.micronaut.jms:micronaut-jms-activemq-artemis")

The JMS ConnectionFactory will be an instance of ActiveMQJMSConnectionFactory. Only the broker URL is configurable in application.yml but further customization is possible with a BeanCreatedEventListener.

5.3 Configuring SQS

To use Amazon SQS, add the following property to your application.yml

micronaut.jms.sqs.enabled=true

and the following to your build.gradle or pom.xml

implementation("io.micronaut.jms:micronaut-jms-sqs")

See the Micronaut AWS docs for more information.

Note that it’s not necessary to add a dependency on io.micronaut.aws:micronaut-aws-sdk-v2 as it’s already a transitive dependency of io.micronaut.jms:micronaut-jms-sqs.

The JMS ConnectionFactory will be an instance of com.amazon.sqs.javamessaging.SQSConnectionFactory; you can customize that with a BeanCreatedEventListener.

Tip
See the guide for Connect a Micronaut JMS Application to an AWS SQS Queue to learn more.

5.4 Configuring Unsupported Brokers

Using an unsupported JMS broker is simple; you simply need to instantiate a ConnectionFactory and JMSConnectionPool in a Bean factory. See the Replacing the ConnectionFactory section below for an example.

With this in place you can configure consumers and producers that use the connection factory by referencing the bean name ("myConnectionFactory" in the example) in @JMSProducer and @JMSListener annotations on your producer interfaces and consumer classes.

5.5 Customizing Brokers

Although only the broker URL is configurable in application.yml, there are potentially many configurable properties in the instantiated ConnectionFactory. Also, you may wish to use a different ConnectionFactory implementation class. Any customization you need can be done by intercepting the creation of the ConnectionFactory with a BeanCreatedEventListener.

Customizing the ConnectionFactory

Here we intercept the creation of the ActiveMQ "Classic" ConnectionFactory by overriding the default to enable asynchronous message sending:

imports
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;
import org.apache.activemq.ActiveMQConnectionFactory;

import jakarta.jms.ConnectionFactory;
@Singleton
public class CustomizeBrokerJMSConnectionPoolListener implements BeanCreatedEventListener<ConnectionFactory> {

    @Override
    public ConnectionFactory onCreated(BeanCreatedEvent<ConnectionFactory> event) {

        ConnectionFactory connectionFactory = event.getBean();
        if (connectionFactory instanceof ActiveMQConnectionFactory) {
            ActiveMQConnectionFactory amqcf = (ActiveMQConnectionFactory) connectionFactory;
            amqcf.setUseAsyncSend(true);
        }

        return connectionFactory;
    }
}

Replacing the ConnectionFactory

Here we discard the default instance and replace it with a custom implementation, in this case an instance of XAConnectionFactory for distributed transaction support.

imports
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Requires;
import io.micronaut.jms.activemq.classic.configuration.properties.ActiveMqClassicConfigurationProperties;
import io.micronaut.jms.annotations.JMSConnectionFactory;
import jakarta.inject.Singleton;
import org.apache.activemq.ActiveMQXAConnectionFactory;
@Factory
class ActiveMQXAConnectionBeanFactory {
    @JMSConnectionFactory("activeMQXAConnectionFactory")
    @Singleton
    ActiveMQXAConnectionFactory createActiveMQXAConnectionFactory(ActiveMqClassicConfigurationProperties amqConfig) {
        return new ActiveMQXAConnectionFactory(amqConfig.getConnectionString());
    }
}

and, then, create a JMSConnectionPool in another factory:

imports
import io.micronaut.context.annotation.EachBean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Primary;
import io.micronaut.jms.configuration.properties.JMSConfigurationProperties;
import io.micronaut.jms.pool.JMSConnectionPool;
import jakarta.inject.Singleton;
import org.apache.activemq.ActiveMQXAConnectionFactory;
@Factory
class JmsConnectionPoolActiveMQXAConnectionFactory {
    private final JMSConfigurationProperties properties;
    JmsConnectionPoolActiveMQXAConnectionFactory(JMSConfigurationProperties properties) {
        this.properties = properties;
    }

    @EachBean(ActiveMQXAConnectionFactory.class)
    @Singleton
    @Primary
    JMSConnectionPool createJmsConnectionPool(ActiveMQXAConnectionFactory connectionFactory) {
        return new JMSConnectionPool(connectionFactory, properties.getInitialPoolSize(), properties.getMaxPoolSize());
    }
}

6 Parameter Binding

Methods in classes/interfaces annotated with @JMSProducer and @JMSListener use parameter binding annotations to access the JMS Message, message body, and message headers.

@MessageBody

The MessageBody annotation is required for the method argument that will be serialized/deserialized as the JMS Message body.

@MessageHeader

The MessageHeader annotation is required for any method arguments that will be serialized/deserialized as JMS headers or non-JMS headers (message properties).

In this example, the message body is a Map and the method supports sending the JMSCorrelationID along with several custom header values:

imports
import org.jspecify.annotations.Nullable;
import io.micronaut.jms.annotations.JMSProducer;
import io.micronaut.jms.annotations.Queue;
import io.micronaut.messaging.annotation.MessageBody;
import io.micronaut.messaging.annotation.MessageHeader;
import java.io.Serializable;
import java.util.Map;

import static io.micronaut.jms.activemq.classic.configuration.ActiveMqClassicConfiguration.CONNECTION_FACTORY_BEAN_NAME;
import static io.micronaut.jms.model.JMSHeaders.JMS_CORRELATION_ID;
@JMSProducer(CONNECTION_FACTORY_BEAN_NAME)
public interface MapProducer {

    @Queue("queue_map")
    void send(@MessageBody Map<String, Serializable> body,
              @MessageHeader(JMS_CORRELATION_ID) @Nullable String correlationId,
              @MessageHeader("CustomStringHeader") @Nullable String stringHeader,
              @MessageHeader("CustomBooleanHeader") boolean booleanHeader,
              @MessageHeader("CustomByteHeader") byte byteHeader,
              @MessageHeader("CustomShortHeader") short shortHeader,
              @MessageHeader("CustomIntegerHeader") int intHeader,
              @MessageHeader("CustomLongHeader") long longHeader,
              @MessageHeader("CustomFloatHeader") float floatHeader,
              @MessageHeader("CustomDoubleHeader") double doubleHeader);
}

This consumer can access the user-supplied header values as well as several headers that can only be set by the JMS provider, e.g. JMSMessageID, JMSPriority, etc.:

imports
import org.jspecify.annotations.Nullable;
import io.micronaut.jms.annotations.JMSListener;
import io.micronaut.jms.annotations.Message;
import io.micronaut.jms.annotations.Queue;
import io.micronaut.messaging.annotation.MessageBody;
import io.micronaut.messaging.annotation.MessageHeader;

import jakarta.jms.Destination;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static io.micronaut.jms.activemq.classic.configuration.ActiveMqClassicConfiguration.CONNECTION_FACTORY_BEAN_NAME;
import static io.micronaut.jms.model.JMSHeaders.JMS_CORRELATION_ID;
import static io.micronaut.jms.model.JMSHeaders.JMS_DELIVERY_MODE;
import static io.micronaut.jms.model.JMSHeaders.JMS_DESTINATION;
import static io.micronaut.jms.model.JMSHeaders.JMS_EXPIRATION;
import static io.micronaut.jms.model.JMSHeaders.JMS_MESSAGE_ID;
import static io.micronaut.jms.model.JMSHeaders.JMS_PRIORITY;
import static io.micronaut.jms.model.JMSHeaders.JMS_REDELIVERED;
import static io.micronaut.jms.model.JMSHeaders.JMS_REPLY_TO;
import static io.micronaut.jms.model.JMSHeaders.JMS_TIMESTAMP;
import static io.micronaut.jms.model.JMSHeaders.JMS_TYPE;
@JMSListener(CONNECTION_FACTORY_BEAN_NAME)
public class MapConsumer {

    List<Map<String, Serializable>> messageBodies = Collections.synchronizedList(new ArrayList<>());
    List<Map<String, Object>> messageHeaders = Collections.synchronizedList(new ArrayList<>());
    List<jakarta.jms.Message> messages = Collections.synchronizedList(new ArrayList<>());

    @Queue(value = "queue_map")
    public void receive(@MessageBody Map<String, Serializable> body,
                        @Message jakarta.jms.Message message,
                        @MessageHeader(JMS_CORRELATION_ID) @Nullable String correlationId,
                        @MessageHeader(JMS_DELIVERY_MODE) int deliveryMode,
                        @MessageHeader(JMS_DESTINATION) Destination destination,
                        @MessageHeader(JMS_EXPIRATION) long expiration,
                        @MessageHeader(JMS_MESSAGE_ID) String messageId,
                        @MessageHeader(JMS_PRIORITY) int priority,
                        @MessageHeader(JMS_REDELIVERED) boolean redelivered,
                        @MessageHeader(JMS_REPLY_TO) @Nullable Destination replyTo,
                        @MessageHeader(JMS_TIMESTAMP) long timestamp,
                        @MessageHeader(JMS_TYPE) @Nullable String type,
                        @MessageHeader("CustomStringHeader") @Nullable String stringHeader,
                        @MessageHeader("CustomBooleanHeader") boolean booleanHeader,
                        @MessageHeader("CustomByteHeader") byte byteHeader,
                        @MessageHeader("CustomShortHeader") short shortHeader,
                        @MessageHeader("CustomIntegerHeader") int intHeader,
                        @MessageHeader("CustomLongHeader") long longHeader,
                        @MessageHeader("CustomFloatHeader") float floatHeader,
                        @MessageHeader("CustomDoubleHeader") double doubleHeader) {

        Map<String, Object> headerValues = new HashMap<>();
        headerValues.put(JMS_CORRELATION_ID, correlationId);
        headerValues.put(JMS_DELIVERY_MODE, deliveryMode);
        headerValues.put(JMS_DESTINATION, destination);
        headerValues.put(JMS_EXPIRATION, expiration);
        headerValues.put(JMS_MESSAGE_ID, messageId);
        headerValues.put(JMS_PRIORITY, priority);
        headerValues.put(JMS_REDELIVERED, redelivered);
        headerValues.put(JMS_REPLY_TO, replyTo);
        headerValues.put(JMS_TIMESTAMP, timestamp);
        headerValues.put(JMS_TYPE, type);
        headerValues.put("CustomStringHeader", stringHeader);
        headerValues.put("CustomBooleanHeader", booleanHeader);
        headerValues.put("CustomByteHeader", byteHeader);
        headerValues.put("CustomShortHeader", shortHeader);
        headerValues.put("CustomIntegerHeader", intHeader);
        headerValues.put("CustomLongHeader", longHeader);
        headerValues.put("CustomFloatHeader", floatHeader);
        headerValues.put("CustomDoubleHeader", doubleHeader);

        messageHeaders.add(headerValues);
        messageBodies.add(body);
        messages.add(message);
    }
}

Session

A listener method can declare a parameter of type jakarta.jms.Session, without a binding annotation, to receive the session that consumed the message, for example to reply on that session. A parameter typed as a provider subtype of Session, such as Oracle’s oracle.jakarta.jms.AQjmsSession from aqapi-jakarta, receives the session when it is of that type. When it is not, the application fails to start. Oracle’s oracle.jms.AQjmsSession, from aqapi, implements javax.jms.Session instead, and is not recognised as a session: like any other listener parameter without a binding annotation, it fails the application at startup.

With transacted = true, the listener commits the session when the method returns and rolls it back when the method throws, so messages sent on the session commit together with the receipt of the message. With Oracle Transactional Event Queues, SQL run on the session’s database connection commits with them too. The following listener confirms each order in the same transaction as its receipt:

7 Serializers and Deserializers

By default, message bodies are converted by DefaultSerializerDeserializer: strings become text messages, maps map messages, byte arrays bytes messages, other serializable objects (lists and object arrays included) object messages, and other objects JSON text messages. A destination can use another conversion with a named Serializer or Deserializer bean:

The serializer member of a producer method names the bean that creates the messages it sends:

The deserializer member of a listener method names the bean that converts the messages it receives into its @MessageBody argument. The other arguments are bound as usual:

8 Error Handlers

There are multiple ways to inject custom error handling into your JMS Listeners as shown here:

9 Success Handlers

There are multiple ways to inject custom success handling into your JMS Listeners as shown here:

10 Message Selectors

Message Selectors allow you to filter the messages a @JMSListener method will receive. The selector can use all message header and properties for filtering except the message content.

@JMSListener(CONNECTION_FACTORY_BEAN_NAME)
public class SelectorConsumer {

    List<String> messageBodiesTrue = Collections.synchronizedList(new ArrayList<>());

    List<String> messageBodiesFalse = Collections.synchronizedList(new ArrayList<>());

    List<String> messageBodiesTopic = Collections.synchronizedList(new ArrayList<>());

    @Queue(value = "selector_queue", messageSelector = "CustomBooleanHeader=true")
    public void receive(@MessageBody String body) {
        messageBodiesTrue.add(body);
    }

    @Queue(value = "selector_queue", messageSelector = "CustomBooleanHeader=false")
    public void receive2(@MessageBody String body) {
        messageBodiesFalse.add(body);
    }

    @Topic(value = "selector_topic", messageSelector = "CustomBooleanHeader=true")
    public void receiveTopic(@MessageBody String body) {
        messageBodiesTopic.add(body);
    }
}

More examples on how to use Message Selector can be found here.

11 Delivery Mode and Priority

Producer methods set the delivery mode and priority of the messages they send with the deliveryMode and priority members of @Queue and @Topic. Messages are PERSISTENT with the JMS default priority, 4, unless a producer method sets otherwise. Listeners ignore both members.

A @MessageDeliveryMode argument can also be the name of a delivery mode, such as "NON_PERSISTENT", and a @MessagePriority argument a String of digits, such as "7". Arguments of other types fail the call. An invalid delivery mode or a priority outside 0 to 9 fails the call with a MessagingClientException.

Listeners read both values with the JMSPriority and JMSDeliveryMode headers, as described under Parameter Binding.

Note
Before 5.2.0, producer methods sent the priority of the message that was just created, which some brokers, such as ActiveMQ Classic, report as 0. They now send 4 unless they set another priority. On a broker that orders messages by priority, these messages are now delivered ahead of messages still sent with priority 0.
Note
Amazon SQS has no delivery modes or priorities, and the SQS JMS client ignores them: jms-sqs producers send their messages without the delivery mode, the priority or the @MessageTTL time to live.

12 Asynchronous Producers and Producer Sessions

Asynchronous producer methods

Producer methods can return a CompletableFuture or another CompletionStage, or a Reactive Streams Publisher such as a Reactor Mono, instead of void:

The propagated context of the caller, for example the current trace with Micronaut Tracing, follows the send to the executor. It is the context of the code that called the method, also when a returned publisher is subscribed to on another thread. Invalid arguments, such as an unknown delivery mode, complete the future exceptionally or make the publisher signal the error, rather than being thrown by the call.

Producer sessions

Producer methods reuse their sessions. For each connection factory, they open one connection and keep up to max-idle idle sessions for each session mode, plain or transacted. Each send borrows a session, so concurrent sends use different sessions, and a session on which a send failed is closed rather than reused. The connection and its sessions are closed with the application context.

At most max-active sessions of a connection factory are borrowed at once, of all session modes. When all of them are borrowed, for example during a burst of asynchronous sends, a send waits up to max-wait for another send to release its session. A session released while sends wait is kept for them, so a burst reuses at most max-active sessions of each session mode rather than closing and opening sessions. Once no send waits, the idle sessions above max-idle are closed: when they are released, or within 30 seconds otherwise. If none is released in time, the send fails with a MessagingClientException: a synchronous method throws it, a returned future completes exceptionally and a returned publisher signals the error. Nothing is sent in that case.

If a session cannot be created, or the JMS provider reports to the connection’s ExceptionListener that the connection failed, the connection is replaced. Its idle sessions are closed, and sessions that sends are still using are closed when they are released, rather than reused. The old connection is closed once the last of its sessions is, so sends in progress on it are not interrupted.

micronaut.jms.producer-sessions.enabled=true
micronaut.jms.producer-sessions.max-idle=8
micronaut.jms.producer-sessions.max-active=32
micronaut.jms.producer-sessions.max-wait=10s

With enabled: false, each send takes a connection from the connection pool and opens a session for that message, as in earlier releases. A JmsProducer created by the application reuses sessions when it is given a ProducerSessionPool.

13 Listener Concurrency and Transacted Sends

A listener method consumes messages on one session, one message at a time. To handle messages concurrently, set consumers on @Queue to the number of consumers to start, each with its own session. The consumers of a listener method share one connection, so the method takes a single connection from the pool however many consumers it has:

Before setting consumers above 1, note that:

  • The listener bean is still a single instance, and its method now runs on several threads at once, one per consumer. The listener must be thread-safe: the example guards its state with a synchronized set and an atomic counter (a lock in Python).

  • Messages are no longer processed in the order they were sent, since each consumer handles the next message it receives while the others are still busy.

  • If a consumer fails to start, the method keeps none of its consumers: the consumers already started are stopped and their connection is given back (returned to the pool, or closed if it is dedicated). A JMSException is logged and the application starts without the method’s consumers; any other failure stops startup.

On a topic, every consumer of a plain subscription receives every message, so consumers above 1 requires a shared subscription (see Durable and Shared Topic Subscriptions), whose consumers share the messages. An unshared durable subscription allows only one consumer. A shared subscription with a client ID can have several consumers: they share the one connection that holds the client ID.

Producer methods send each message on a session of their own. With transacted = true, the session is transacted and the send is committed before the method returns:

To send messages in the transaction of a transacted listener, so that they are committed or rolled back with the message it received, send them with a producer of the listener’s Session, as shown under Parameter Binding.

14 Health, Metrics and Tracing

Health

With the micronaut-management module, the health endpoint has a jms indicator. For each JMS connection factory, it opens a new connection, not one from the pool, on the blocking executor. The indicator is UP when every connection factory connects, with the JMS provider name and version of each, and DOWN with the error otherwise. It only checks that a connection can be opened, which doesn’t reach the broker with every provider: an ActiveMQ Classic failover: URL connects in the background, and the SQS connection factory makes no AWS call, so the indicator reports UP for them while the broker is unreachable.

Add the dependency:

implementation("io.micronaut:micronaut-management")

Disable the indicator with:

endpoints.health.jms.enabled=false

Metrics

With a Micrometer MeterRegistry, for example from the Micronaut Micrometer module, the following meters are recorded:

Meter Description

jms.listener.messages

Counter of the messages that listeners process

jms.listener.duration

Timer of the processing of a message, from the start of the listener method to the end of the success or error handlers

jms.producer.messages

Counter of the messages that producer methods and JmsProducer send

jms.producer.duration

Timer of the sending of a message, from taking the connection to the commit of a transacted send, so a send that fails to connect is counted as a failure too

The meters are tagged with the destination and the result: success, or failure with the exception class name. Disable the metrics with:

micronaut.metrics.binders.jms.enabled=false

Tracing

To trace messages with OpenTelemetry, add the micronaut-jms-tracing-opentelemetry module, which uses the OpenTelemetry bean of Micronaut Tracing:

implementation("io.micronaut.jms:micronaut-jms-tracing-opentelemetry")

Messages are then traced:

  • Sending a message creates a PRODUCER span named send <destination>, a child of the current span. It starts before the connection is taken, so a send that fails to connect ends it with an error too. The trace context is propagated to the receiver in message properties by the configured propagators.

  • Processing a message creates a CONSUMER span named process <destination> that continues the trace of the sender. It is the current span while the listener method and the success and error handlers run, so spans created by the listener method are its children. It is also current in Micronaut’s propagated context, so it follows the listener method to the threads that the context propagates to. A failure sets the span’s status to ERROR and records the exception.

The spans have the messaging.system (jms), messaging.destination.name, messaging.operation.name, messaging.operation.type and messaging.message.id attributes. JMS property names must be Java identifiers, so the dashes of propagation headers such as those of B3 are replaced with __dash__ in property names, as the OpenTelemetry Java agent does. Disable tracing with:

micronaut.jms.tracing.enabled=false
Note
The OpenTelemetry Java agent has its own JMS instrumentation, which also creates spans for the messages that are sent and processed. Micronaut Tracing uses the agent’s OpenTelemetry when the agent is attached, so an application that runs with the agent should not add micronaut-jms-tracing-opentelemetry, or each message would be traced twice.

Custom observers

The metrics and the tracing are beans of type JMSMessageObserver. Other beans of that type are also called before a listener processes a message and before a producer sends one, and the observation each returns is completed afterwards with the error, if any. A producer’s observation starts before the connection is taken, and its beforeSend method is called with the message once the message is created, so properties set on it are sent. A listener’s observation can give, from its propagatedContext method, the propagated context that the listener method and the handlers run in. Observers are called in order, and their observations are completed in the reverse order. A JmsProducer created by the application only calls the observers added with addObservers.

15 Durable and Shared Topic Subscriptions

By default a @Topic listener method creates a non-durable subscription: it receives only the messages published while it is connected, and every listener receives every message.

Set subscription to name the subscription, then choose its kind with durable and shared:

durable shared Subscription

false

false

Non-durable (the default). Do not set subscription.

true

false

Durable, unshared (Session.createDurableSubscriber). The broker keeps messages for the subscription while the listener is disconnected.

false

true

Shared, non-durable (Session.createSharedConsumer, JMS 2.0). Every consumer of the subscription, in this or another application instance, receives a share of the messages.

true

true

Shared, durable (Session.createSharedDurableConsumer, JMS 2.0). Messages are split between the consumers and kept while all of them are disconnected.

The following listener keeps an audit of every order, including the ones published while the application was down:

Client IDs

JMS identifies an unshared durable subscription by the connection’s client ID together with the subscription name, and a client ID can be set only once per connection. Micronaut JMS therefore gives each unshared durable listener its own connection, outside the connection pool, with the client ID set from clientId. When clientId is not set, the subscription name is the client ID.

Set clientId when a broker administrator has assigned one, or to give a shared subscription a client ID. A listener with a clientId always gets its own connection. Shared subscriptions without one use a pooled connection.

Note
A client ID can be in use by only one connection at a time. Do not run two instances of an application with the same unshared durable listener against one broker; use a shared durable subscription to spread the messages between instances.

durable, shared and clientId require subscription, and subscription requires durable or shared. Micronaut JMS fails at startup if either is missing. Shared subscriptions need a JMS 2.0 broker, such as ActiveMQ Artemis. ActiveMQ "Classic" supports only unshared durable subscriptions: a shared listener on it fails to register, which is logged like any other registration failure.

16 Listener Session Recovery

A listener recovers when the JMS provider reports that the session of one of its consumers died, so that the consumer doesn’t stop receiving messages until the application restarts.

What Triggers Recovery

When the JMS provider reports an error through the ExceptionListener of a listener’s connection, each consumer of that connection checks its session by looking up its destination. Only a consumer whose session fails that check recovers. Errors that leave the session usable don’t recreate anything, such as the rejected asynchronous send that ActiveMQ "Classic" reports while its connection still works.

For example, when the database session of an Oracle AQ listener is disconnected, AQ reports JMS-120: Dequeue failed, and later JMS-112: Connection is invalid on each of its pings. A new session on the same connection still works, so the listener recovers.

What Is Recreated

The consumer closes its session and MessageConsumer and creates them again on the same connection, with the same message selector and subscription. A durable or shared subscription resumes on the same subscription, and the provider redelivers the messages that weren’t acknowledged.

If the session or consumer can’t be created, the consumer tries again after a delay that doubles after each attempt, up to a maximum. It keeps trying until it succeeds or the application shuts down, since the provider may report nothing more once the failed session is closed. io.micronaut.jms.listener.JMSListenerRegistry logs the first attempts as warnings and later ones as errors.

micronaut.jms.listener-recovery.enabled=true
micronaut.jms.listener-recovery.warn-attempts=10
micronaut.jms.listener-recovery.initial-delay=1s
micronaut.jms.listener-recovery.max-delay=1m
  • enabled turns recovery on or off.

  • warn-attempts is the number of failed attempts logged as warnings. Later attempts are logged as errors.

  • initial-delay is the delay after the first failed attempt. It must be positive.

  • max-delay is the longest delay between two attempts. It must not be shorter than initial-delay.

An invalid value fails the registration of the listeners with a ConfigurationException. Only the first attempt logged as an error includes the stack trace, unless debug logging is enabled for io.micronaut.jms.listener.JMSListenerRegistry.

Limits

  • A non-durable subscription loses the messages published while its consumer is recreated.

  • A connection that fails as a whole isn’t replaced. For example, when an ActiveMQ broker restarts without a failover transport, only the session is recreated, on the same connection.

  • A consumer that is still recovering doesn’t change the jms health indicator.

17 GraalVM support

Micronaut JMS is compatible with GraalVM. Everything is handled automatically by the library so users don’t need any special configuration.

Note
See the section on GraalVM in the user guide for more information.

18 Repository

You can find the source code of this project in this repository: