Use JMS for asynchronous messaging in Spring Boot

Java Message Service (JMS) is an API that provides a standardized way for Java applications to create, send, receive, and read messages in messaging systems. Spring JMS uses idiomatic APIs to produce and consume messages with JMS, using the JMSTemplate class and @JMSListener annotations for producers and consumers, respectively.

In this article, we’ll cover a producer/consumer implementation using Spring JMS with Oracle Database Transactional Event Queues. At the end of the article, we’ll tie the example together with a comprehensive test suite using Testcontainers to see real-time message processing in action.

If you’re unfamiliar with Transactional Event Queues, it’s a high-throughput messaging system capable of multiple producers/consumers and exactly-once messaging. I’ve published a more detailed write-up Here.

Want to skip the article and go straight to the code? Find the full sample on GitHub Here.

Configuring JMS permissions and creating a JMS queue

The following SQL script grants the necessary permissions to use Transactional Event Queues with JMS to a database user, and then creates a Transactional Event Queue for use by that user. We’ll dynamically invoke this script in our test later on, and it is provided here for reference purposes.

-- Set as appropriate for your database.
alter session set container = freepdb1;

-- Configure testuser with the necessary privileges to use Transactional Event Queues.
grant aq_user_role to testuser;
grant execute on dbms_aq to testuser;
grant execute on dbms_aqadm to testuser;
grant execute ON dbms_aqin TO testuser;
grant execute ON dbms_aqjms TO testuser;

-- Create a Transactional Event Queue
begin
    -- See https://docs.oracle.com/en/database/oracle/oracle-database/23/arpls/DBMS_AQADM.html#GUID-93B0FF90-5045-4437-A9C4-B7541BEBE573
    -- For documentation on creating Transactional Event Queues.
    dbms_aqadm.create_transactional_event_queue(
            queue_name         => 'testuser.testqueue',
            -- Payload can be RAW, JSON, DBMS_AQADM.JMS_TYPE, or an object type.
            -- Default is DBMS_AQADM.JMS_TYPE.
            queue_payload_type => DBMS_AQADM.JMS_TYPE,
            -- FALSE means queues can only have one consumer for each message. This is the default.
            -- TRUE means queues created in the table can have multiple consumers for each message.
            multiple_consumers => false
    );

    -- Start the queue
    dbms_aqadm.start_queue(
            queue_name         => 'testuser.testqueue'
    );
end;
/

Note that queues must be started using the dbms_aqadm.start_queue procedure before they are operational.

JMS Producer/Consumer Application

The main dependencies you’ll need are spring-boot-starter-data-jdbc and oracle-spring-boot-starter-aqjms. The oracle-spring-boot-starter-aqjms dependency pulls in all necessary packages to use Spring JMS with Oracle Database Transactional Event Queues, requiring minimal configuration.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.oracle.database.spring</groupId>
    <artifactId>oracle-spring-boot-starter-aqjms</artifactId>
    <version>25.3.0</version>
</dependency>

If you’re using Oracle Database Wallet for authentication, make sure to include the oracle-spring-boot-starter-wallet dependency

<dependency>
    <groupId>com.oracle.database.spring</groupId>
    <artifactId>oracle-spring-boot-starter-wallet</artifactId>
    <version>25.3.0</version>
</dependency>

Find the full pom.xml used in the example Here.

Application properties

Because we’re using Oracle Database, we configure our application properties to use Oracle Database as a Spring Datasource, parameterizing the JDBC url, username, and password. We’ll override these values later while testing.

spring:
  datasource:
    username: ${USERNAME}
    password: ${PASSWORD}
    url: ${JDBC_URL}

    # Set these to use UCP over Hikari.
    driver-class-name: oracle.jdbc.OracleDriver
    type: oracle.ucp.jdbc.PoolDataSourceImpl
    oracleucp:
      initial-pool-size: 1
      min-pool-size: 1
      max-pool-size: 30
      connection-pool-name: UCPSampleApplication
      connection-factory-class-name: oracle.jdbc.pool.OracleDataSource

Implementing the JMS producer

Our JMS producer class makes use of Spring’s JMSTemplate, which is available via autowiring. The JMSTemplate class provides a simple API for working with JMS — here we use the convertAndSend method to produce messages to a queue.

package com.example;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;

@Component
public class Producer {
    private final JmsTemplate jmsTemplate;
    private final String queueName;

    public Producer(JmsTemplate jmsTemplate,
                    @Value("${txeventq.queue.name:testqueue}") String queueName) {
        this.jmsTemplate = jmsTemplate;
        this.queueName = queueName;
    }

    public void enqueue(String message) {
        jmsTemplate.convertAndSend(queueName, message);
    }
}

Implementing the JMS consumer

The JMSTemplate class can also be used to receive messages and provides a variety of JMS functionality.

Our JMS Consumer class uses Spring’s @JmsListener annotation to set up a message receiver for a given queue — in this case, a queue named “testqueue”. The basic consumer prints each message it receives to stdout.

For verification purposes, we add a CountDownLatch to the consumer, so we can poll it for completion after a certain amount of messages are processed — We’ll use this capability in our example test.

package com.example;

import java.util.concurrent.CountDownLatch;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

@Component
public class Consumer {
    private final CountDownLatch latch;

    public Consumer(@Value("${txeventq.consumer.numMessages:5}") int numMessages) {
        latch = new CountDownLatch(numMessages);
    }

    @JmsListener(destination = "${txeventq.queue.name:testqueue}", id = "sampleConsumer")
    public void receiveMessage(String message) {
        System.out.printf("Received message: %s%n", message);
        latch.countDown();
    }

    public void await() throws InterruptedException {
        latch.await();
    }
}

App entry point

The last thing our example needs is a Spring Boot main class. Let’s define that here:

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SampleApp {
    public static void main(String[] args) {
        SpringApplication.run(SampleApp.class, args);
    }
}

The bean definition is essential for the Spring JMS configuration, ensuring the JMS connections are configured for Oracle Database.

Wiring things together with an example test

The sample provides an all-in-one test leveraging Testcontainers and Oracle Database to do the following:

  1. Start and configure a database server using Testcontainers
  2. Produce several messages to a Transactional Event Queue using an autowired JMSTemplate.
  3. Verify all messages are consumed by the JMSListener consumer.
package com.example;

import java.time.Duration;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jms.config.JmsListenerEndpointRegistry;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.oracle.OracleContainer;
import org.testcontainers.utility.MountableFile;

import static org.junit.jupiter.api.Assertions.assertTimeout;

@Testcontainers
@SpringBootTest
public class SpringJmsTest {
    /**
     * Use a containerized Oracle Database instance for testing.
     */
    static OracleContainer oracleContainer = new OracleContainer("gvenzl/oracle-free:23.5-slim-faststart")
            .withStartupTimeout(Duration.ofMinutes(5))
            .withUsername("testuser")
            .withPassword(("testpwd"));

    /**
     * Set up the test environment:
     * 1. configure Spring Properties to use the test database.
     * 2. run a SQL script to configure the test database for our JMS example.
     */
    @BeforeAll
    static void setUp() throws Exception {
        oracleContainer.start();

        // Dynamically configure Spring Boot properties to use the Testcontainers database.
        System.setProperty("JDBC_URL", oracleContainer.getJdbcUrl());
        System.setProperty("USERNAME", oracleContainer.getUsername());
        System.setProperty("PASSWORD", oracleContainer.getPassword());

        // Configures the test database, granting the test user access to TxEventQ, creating and starting a queue for JMS.
        oracleContainer.copyFileToContainer(MountableFile.forClasspathResource("init.sql"), "/tmp/init.sql");
        oracleContainer.execInContainer("sqlplus", "sys / as sysdba", "@/tmp/init.sql");
    }

    @Autowired
    private Producer producer;

    @Autowired
    private Consumer consumer;

    @Autowired
    private JmsListenerEndpointRegistry jmsListenerEndpointRegistry;

    @Test
    void springBootJMSExample() {
        System.out.println("Starting Spring Boot JMS Example");

        for (int i = 1; i < 6; i++) {
            producer.enqueue("test message %d".formatted(i));
        }
        System.out.println("Produced 5 messages to the queue via JMS.");

        System.out.println("Waiting for consumer to finish processing messages...");
        assertTimeout(Duration.ofSeconds(5), () -> {
            consumer.await();
        });

        // Do a clean shutdown of the JMS listener.
        jmsListenerEndpointRegistry.getListenerContainer("sampleConsumer").stop();
        System.out.println("Consumer finished.");
    }
}




You can run the test like so, from the project’s root directory. Note that a Docker compatible environment is required to run the test:

mvn test

Running the test, you should see output similar to the following:

Starting Spring Boot JMS Example
Produced 5 messages to the queue via JMS.
Waiting for consumer to finish processing messages...
Received message: test message 1
Received message: test message 2
Received message: test message 3
Received message: test message 4
Received message: test message 5
Consumer finished.

Questions? Let me know in the comments, or reach out directly.

Additional Resources

Leave a Reply

Discover more from andersswanson.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading