Java Spring Boot project with embedded ActiveMQ JMS provider & distributed JTA / XA transactions manager.

Recently I’ve been trying to configure the new Spring Boot project to work with embedded ActiveMQ to send and receive JMS messages. To send messages I used Spring JmsTemplate. For simplicity, I send simple String messages:

Nowadays Apache Kafka is popular as a fast and reliable messages broker. However, if you need to make deal with transactions it is better to rely on old-good (and often heavyweight) JMS solutions. Also, it is possible to use ActiveMQ as an embedded JMS broker to your Spring Boot application without complex environment configuration which is required for Kafka.

So, to test transactions behavior I decided to mix JMS and database operations and see how Spring will handle transactions rollback in case of any error in the database query.

Whole project sources are available at my Github: https://github.com/vlytsus/boot-jms

Spring supports @Transactional to wrap annotated method to transaction. It uses CGLIB proxies to automatically create transaction before method call and automatically commit after method execution, or rollback if an exception occurs. And if you perform several database operations inside such method they all will be rolled back if one operation fails. But what happens with JMS messages if we will try to send them in the same transactional method which fails?

Please see example in my Git repository: https://github.com/vlytsus/boot-jms/blob/master/src/main/java/com/jms/command/UserServiceImpl.java

Call to sendUser(username) should fail because required username field is null: userEntity2.setUsername(null); And database state will be rolled back if we try to insert several entities.

Example of @Transactiona method that combines JMS & Database transaction

There are several endpoints exposed to test transactions from different perspectives. By default, the project should be configured to not use JTA/XA transaction management (spring-boot-starter-jta-atomikos dependency is not active). In that case, Spring Framework uses its own local declarative transactions. Let’s call following endpoints in browser to create users “user01”, “user02” and “user03”:

Last query “users/all” will print all users inserted to the database:

In logs you will also see that JMS consumer has received some messages:

As you see operation “transactional/with_clone/create” inserts 2 records. It will be used later to demonstrate data rollback.

Now let’s modify our queries to not provide any usernames for new users. Which should lead to database insertion errors because field username must not be null:

All create operations are failed and no data inserted into the database. Even method “transactional/with_clone/create” has not stored user record with username “_clone”. Because the second insert with null username causes an exception and rollback of the whole transaction together with inserted “_clone” user. However, despite database state was rolled back, messages were processed by JMS. Because Spring local transactions have no influence on JMS consumers.

Spring default transaction rollback can’t stop consumer from message consuming if at the moment of an error on step 3 message was already consumed in step 2.

So, if If you want to mix database and JMS transactions together and make truly atomic operations you should handle transactions by some distributed JTA/XA transactions manager.

JTA External Transaction Manager will ensure that consumer will not receive a message until the transaction is successfully committed

To configure external manager I used popular embeddable cloud-native transaction manager — Atomikos.

To activate it you should uncomment Atomikos dependency in pom.xml

Please rebuild and restart the application after that. If you repeat the last test (without usernames) you will notice that JMS messages are not consumed anymore in case of a database error.

There are no corresponding logs like “received payload: “ because transaction rollback was propagated to JMS consumer too by Atomikos JTA/XA transactions manager.

You can try again with normal usernames to make sure that everything works fine, like during the first test if some usernames are provided:

And you’ll see JMS consumed messages in logs:

Conclusion

Spring allows using @Transactional annotations to handle local transactions on the JDBC level. However, if you want to have global distributed transactions with several resources like databases & JMS consumers you have to configure JPA Transaction Management Provider.

Software Developer & Team Leader