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.OracleDataSourceImplementing 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:
- Start and configure a database server using Testcontainers
- Produce several messages to a Transactional Event Queue using an autowired JMSTemplate.
- 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.

Leave a Reply