Search code examples
spring-bootjpajmsjtaspring-boot-jpa

@JmsListener and persisting to database


I'm working on a service where I listen to the queue, deserialize received messages, and persist them to a database (Oracle). Roughly:

@JmsListener(destination="some-destination")
public void onMessage(Message message) throws Exception {
    String message = ((TextMessage) message).getText();
    service.save(deserialize(message));
    // includes exception handling etc
}

In the default message listener bean I set the concurrency and setSessionTransacted(true). Is this enough to make the whole onMessage transactional? So that a message is received and saved in one transaction, and rolled back if there's a failure at any of these points? I tried throwing exceptions on specific messages when there was an attempt to save them - and the messages were indeed rolled back to the queue and the listener tried to consume them again, which is a desired behavior. When researching this, I stumbled upon distributed transactions, jta transaction manager, but I am still not sure whether more needs to be configured apart from setSessionTransacted(true) or whether Spring Boot automatically handles transactions of XA resources. Looking for advice. Thank you.


Solution

  • If your listener's onMessage() is interacting with transactional resources other than the JMS broker from which it received the message than calling setSessionTransacted(true) is not enough to make all the interactions transactional.

    A "transacted" session in JMS only covers JMS operations with that specific session. It doesn't cover work with any other transactional resource (e.g. a database).

    If you want a transaction which involves the consumption of the message by the listener along with any other transactional work done by the listener (e.g. updating or inserting records in a database, sending JMS messages to another broker, etc.) then you need a JTA transaction manager which can work with XA resources in order to coordinate the various transaction phases (e.g. prepare, commit, rollback). These kinds of transactions are sometimes called "distributed" transactions.

    This is a fairly common use-case as it makes a JMS message into a kind of "unit of work" and you know if the message was consumed then all the work related to that message was also completed successfully and vice-versa. This is one of the main things that MDBs provide in Java EE, but the same essential work can be done in Spring as well.

    According to the Spring Boot documentation you can integrate with a handful of different transaction managers (e.g. Atomikos, Bitronix, Narayana, etc.) to do this kind of work.

    To be clear, in some situations your existing arrangement will work in such a way as to make it seem like the two operations are in the same transaction. For example, if your database operation throws an exception and that exception gets thrown from the onMessage() then the message will be rolled back onto the queue. However, in this situation the two operations are just correlated. They are not in the same transaction so they won't actually be atomic. If instead the database operation had succeeded and then for some reason another exception was thrown from the listener's onMessage() or the JMS broker crashed before the transacted session could be committed then the message would ultimately be rolled back onto the queue but the data would also still be in the database so that if you consumed the message again you'd ostensibly write the same data to the database again.