JMS Point-to-Point Example: Queues With Jakarta JMS

Send messages to a JMS queue and receive them with Jakarta Messaging 3.1 and ActiveMQ Artemis, with competing consumers, acknowledgment modes, transactions and a QueueBrowser.

In JMS point-to-point messaging, a producer sends each message to a queue, and only one consumer takes that message off the queue. The queue stores the message until a consumer receives it, so the consumer does not have to run at the moment the message is sent.

We use a queue for work that must be done once, such as resizing an uploaded photo or charging one payment. Several workers can read from the same queue, and the broker splits the messages between them.

The following example sends two photo names to the queue thumbnails and reads them back in a worker. It uses Jakarta Messaging 3.1 (the new name of JMS) with an ActiveMQ Artemis 2.57.0 broker on Java 25.

try (JMSContext context = factory.createContext()) {
  Queue queue = context.createQueue("thumbnails");

  JMSProducer producer = context.createProducer();
  producer.send(queue, "beach.jpg");
  producer.send(queue, "cake.jpg");

  JMSConsumer worker = context.createConsumer(queue);
  String first = worker.receiveBody(String.class, 1000);    // "beach.jpg"
  String second = worker.receiveBody(String.class, 1000);   // "cake.jpg"
  String third = worker.receiveBody(String.class, 1000);    // null (queue is empty)
}

Notice that the worker is created after both send() calls and still gets both messages in order. The third call returns null after one second, because each message leaves the queue once a consumer has received it.

In the rest of the post, we set up the broker and look at the ways to receive from a queue. After that, we run two competing workers, and we use acknowledgments and transactions so that a failed job goes back to the queue instead of getting lost.

1. What Point-to-Point Messaging Means

A JMS queue is a destination that keeps messages in the order they arrive and gives each message to one receiver. The receiver confirms the message with an acknowledgment, which allows the broker to delete it.

Client 1 sends a message to a queue. Client 2 consumes the message from the queue and sends an acknowledgment back.
A queue gives each message to one consumer, which acknowledges it after processing.

Consider a photo sharing app. Every upload needs a small thumbnail, and creating one takes too long to do inside the upload request. The upload request puts the photo name on the thumbnails queue and returns to the user right away, and a separate worker process creates the thumbnails one after another. If the worker is down for a few minutes, the jobs wait in the queue and nothing is lost.

The point-to-point model follows a few rules that the rest of the article builds on.

  • Each message reaches only one consumer, even when several consumers read from the queue.
  • The queue keeps a message until a consumer acknowledges it or the message expires.
  • A message that is received but never acknowledged goes back to the queue and is delivered again.
  • The producer and the consumer do not need to run at the same time.

When every interested service must get its own copy of a message, we use a topic instead, as in the JMS publish/subscribe example.

2. Maven Dependencies and the Artemis Broker

Jakarta Messaging defines the API in the jakarta.jms package, and a broker product implements it. The client library of ActiveMQ Artemis brings jakarta.jms-api 3.1.0 as a dependency, and the server module lets us run the broker inside our JVM for tests and demos.

<dependency>
  <groupId>org.apache.artemis</groupId>
  <artifactId>artemis-jakarta-client</artifactId>
  <version>2.57.0</version>
</dependency>
<!-- only for the embedded broker in tests and demos -->
<dependency>
  <groupId>org.apache.artemis</groupId>
  <artifactId>artemis-jakarta-server</artifactId>
  <version>2.57.0</version>
</dependency>

Older tutorials use the groupId org.apache.activemq for Artemis. Maven still resolves it, but it prints a relocation warning, because the Artemis artifacts moved to org.apache.artemis.

The embedded broker listens on tcp://localhost:61616, which is also the default port of a standalone Artemis broker. We turn off persistence and security for the demo, so we never use the following configuration in production.

Configuration config = new ConfigurationImpl()
    .setPersistenceEnabled(false)
    .setSecurityEnabled(false)
    .addAcceptorConfiguration("tcp", "tcp://localhost:61616");

EmbeddedActiveMQ broker = new EmbeddedActiveMQ();
broker.setConfiguration(config);
broker.start();

ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");

For a standalone broker, we run bin/artemis create mybroker in the Artemis install folder and start the broker with mybroker/bin/artemis run. The client code stays the same, apart from the user name and password that we pass to createContext(user, password).

3. Sending Messages to a Queue

A JMSContext holds one connection and one session. We create it in a try-with-resources block, so the connection is closed even when sending fails.

The method createQueue() returns an object with the queue name and does not create anything on the broker. Artemis creates the queue the first time a producer or consumer uses it, because auto-creation is turned on by default. The methods of JMSProducer return the producer itself, so we can chain several send() calls.

try (JMSContext context = factory.createContext()) {
  Queue queue = context.createQueue("thumbnails");
  context.createProducer()
      .send(queue, "tree.jpg")
      .send(queue, "lake.jpg");
}

By default, the producer sends PERSISTENT messages, which the broker stores on disk and keeps after a restart. For jobs that are worthless after some time, we also set a time to live with setTimeToLive(60_000), and the broker drops the message when nobody has received it within 60 seconds.

4. Receiving Messages From a Queue

Because a queue stores its messages, a consumer that starts later still gets everything that was sent before. The following example sends dog.jpg while no consumer is running, closes the producer’s context, and starts a consumer afterwards.

// 1. Producer sends while no consumer is running
try (JMSContext context = factory.createContext()) {
  Queue queue = context.createQueue("thumbnails");
  context.createProducer().send(queue, "dog.jpg");
}

// 2. Consumer starts later and still gets the message
try (JMSContext context = factory.createContext()) {
  Queue queue = context.createQueue("thumbnails");
  String body = context.createConsumer(queue).receiveBody(String.class, 1000);   // "dog.jpg"
}

4.1. Synchronous Receive Methods

A JMSConsumer offers several methods that wait for the next message on the calling thread. They differ in how long they wait and what they return.

MethodWaitsReturns when no message arrives
receive()until a message arrives or the consumer closesnever returns while the queue is empty
receive(1000)up to 1000 msnull
receiveNoWait()does not waitnull
receiveBody(String.class, 1000)up to 1000 ms, returns only the bodynull

In production code, we always pass a timeout, because receive() without one blocks the thread forever on an empty queue. If the body does not match the requested type, receiveBody() throws a MessageFormatRuntimeException.

4.2. Asynchronous Receive With a MessageListener

A worker that runs all day uses a MessageListener. The client library calls onMessage() on its own thread for each message, so we do not write a polling loop. The demo waits on a CountDownLatch until both jobs are done.

CountDownLatch done = new CountDownLatch(2);

try (JMSContext consumerContext = factory.createContext();
     JMSContext producerContext = factory.createContext()) {

  Queue queue = consumerContext.createQueue("thumbnails");
  JMSConsumer worker = consumerContext.createConsumer(queue);
  worker.setMessageListener(message -> {
    try {
      System.out.println("Resizing " + message.getBody(String.class));
    } catch (JMSException e) {
      System.out.println("Bad message: " + e.getMessage());
    } finally {
      done.countDown();
    }
  });

  producerContext.createProducer().send(queue, "tree.jpg").send(queue, "lake.jpg");
  boolean all = done.await(5, TimeUnit.SECONDS);
  System.out.println("All jobs done: " + all);
}
Resizing tree.jpg
Resizing lake.jpg
All jobs done: true

Message listeners are allowed in Java SE programs such as this worker. Inside a Jakarta EE server, web components and EJBs may not call setMessageListener(), so a message-driven bean receives the queue messages there.

5. Competing Consumers on One Queue

When one worker cannot keep up, we start more workers on the same queue, and these workers are called competing consumers. The broker gives each message to one of the workers, so the jobs are spread over them and no job runs twice.

The following example creates two workers, each with its own JMSContext, and sends four photos to the queue.

JMSConsumer worker1 = ctx1.createConsumer(queue);
JMSConsumer worker2 = ctx2.createConsumer(queue);

JMSProducer producer = producerCtx.createProducer();
for (String photo : new String[] {"a.jpg", "b.jpg", "c.jpg", "d.jpg"}) {
  producer.send(queue, photo);
}
worker 1 resized a.jpg
worker 2 resized b.jpg
worker 1 resized c.jpg
worker 2 resized d.jpg

We can see that Artemis gives the messages to the two workers in turn. The split is fair only when both workers are equally fast, though. By default, every Artemis consumer buffers up to 1 MiB of messages on the client side, so a slow worker can hold jobs that a free worker could already process. For slow jobs, we turn the buffer off with consumerWindowSize=0 in the URL, as in tcp://localhost:61616?consumerWindowSize=0, which the Artemis flow control page describes.

6. Acknowledgments, Transactions and Redelivery

An acknowledgment tells the broker that the consumer has processed a message, and only then does the broker delete it. Until the acknowledgment, the broker treats the message as in flight. If the consumer closes or crashes first, the broker delivers the message again.

Say the worker receives sky.jpg and the JVM crashes in the middle of resizing. With the wrong mode, the message was already acknowledged on receipt, and the thumbnail is never created. The session mode, which we pass to createContext(), decides when the acknowledgment happens.

Session modeWhen the message is acknowledged
JMSContext.AUTO_ACKNOWLEDGE (default)when receive() returns, or when onMessage() returns
JMSContext.CLIENT_ACKNOWLEDGEwhen we call context.acknowledge()
JMSContext.DUPS_OK_ACKNOWLEDGElazily, so a message may be delivered twice
JMSContext.SESSION_TRANSACTEDwhen we call context.commit(); rollback() returns the messages

6.1. Acknowledging Messages Ourselves

With CLIENT_ACKNOWLEDGE, the worker calls acknowledge() after the job is done. The following example receives sky.jpg without acknowledging it and closes the context, as a crash would.

// 1. Receive without acknowledging, then close
try (JMSContext context = factory.createContext(JMSContext.CLIENT_ACKNOWLEDGE)) {
  Queue queue = context.createQueue("thumbnails");
  String body = context.createConsumer(queue).receiveBody(String.class, 1000);    // "sky.jpg"
}

// 2. The broker delivers the message again
try (JMSContext context = factory.createContext(JMSContext.CLIENT_ACKNOWLEDGE)) {
  Queue queue = context.createQueue("thumbnails");
  String again = context.createConsumer(queue).receiveBody(String.class, 1000);   // "sky.jpg"
  context.acknowledge();
}

The call context.acknowledge() confirms every message that the context has received so far, not only the last one.

6.2. Rolling Back a Transacted Session

In a transacted session, commit() acknowledges the received messages, and rollback() puts them back on the queue. The broker sets the header JMSRedelivered and increases the property JMSXDeliveryCount on every new delivery, so a worker can see how often a job has failed.

try (JMSContext context = factory.createContext(JMSContext.SESSION_TRANSACTED)) {
  Queue queue = context.createQueue("thumbnails");
  JMSConsumer worker = context.createConsumer(queue);

  Message first = worker.receive(1000);
  context.rollback();                     // resize failed, put the message back

  Message second = worker.receive(1000);
  context.commit();                       // resize worked, remove the message
}
1st try: broken.jpg, redelivered = false, delivery count = 1
2nd try: broken.jpg, redelivered = true, delivery count = 2

A job that fails every time would be delivered forever. Artemis stops after 10 delivery attempts by default, and after the last attempt it either deletes the message or moves it to a dead letter address, if one is configured. The Artemis redelivery settings also add a delay between attempts, which is 0 by default.

7. Browsing a Queue Without Consuming

A QueueBrowser reads the messages that wait in a queue without removing them. We use it in admin pages and health checks, for example to show how many thumbnails are still waiting.

try (QueueBrowser browser = context.createBrowser(queue)) {
  Enumeration<?> messages = browser.getEnumeration();
  int waiting = 0;
  while (messages.hasMoreElements()) {
    Message message = (Message) messages.nextElement();
    System.out.println("waiting: " + message.getBody(String.class));
    waiting++;
  }
}
waiting: x.jpg
waiting: y.jpg
waiting count = 2
after browse, first = x.jpg

After browsing, a normal consumer still receives x.jpg first, because the browser did not take anything off the queue. The spec does not require the browser to show a fixed snapshot, so messages that arrive or expire during browsing may or may not appear in the list.

8. JMS Point-to-Point FAQs

8.1. Does the Receiver Have to Run When the Message Is Sent?

No. The queue stores the message until a consumer connects and receives it, as the dog.jpg example in section 4 shows. Only an expired message is dropped.

8.2. Can Two Consumers Receive the Same Message From a Queue?

No, not in normal operation. The queue gives each message to one consumer, and two consumers on one queue share the work, as in section 5. A message reaches a second consumer only when the first one does not acknowledge it. When two services must both process every message, we send the message to a topic.

8.3. What Happens When onMessage() Throws an Exception?

In AUTO_ACKNOWLEDGE mode, the broker delivers the message again right away. For example, a listener that throws a RuntimeException on the first attempt gets flaky.jpg a second time, with JMSRedelivered set to true. The Jakarta Messaging spec calls a throwing listener a programming error, so we catch exceptions in onMessage() and move unprocessable messages to an error queue ourselves.

8.4. Why Does receive() Return null?

Either the timeout expired with an empty queue, or the connection was never started. With the classic API, a Connection delivers no messages until we call connection.start(). A JMSContext starts its connection when we create the first consumer, so the simplified API needs no start() call.

9. Conclusion

In point-to-point messaging, a producer puts each message on a queue, and only one consumer receives it. The queue keeps messages until they are acknowledged, so producers and consumers can run at different times.

We send and receive with a JMSContext, either with a timeout or with a MessageListener. To process more jobs, we add competing consumers on the same queue and keep the client-side buffer small when the jobs are slow.

A job must not get lost when the worker fails, so we choose the acknowledgment mode on purpose. With CLIENT_ACKNOWLEDGE or a transacted session, an unfinished job returns to the queue, and JMSXDeliveryCount shows how often it has failed. For the basic JMS building blocks, see the JMS tutorial, and for Spring applications, the Spring Boot JmsTemplate with ActiveMQ example.

10. References

Happy Learning !!

Leave a Comment

  1. I have a Point to Point queue set up. I can add messages to the queue using a commit transaction. When reading I would like to process each message and then acknowledge the message has been processed. I can use MessageConsumer.receive to read new messages but the messages are read faster than they are processed. When I acknowledge what I think is a single message I get several messages acknowledged, one I am looking at and others that have been received from the queue but not yet processed.

    How do I acknowledge just the message in the TextMessage variable?

  2. javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial

    this types of error is comimng what to do.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.