Montag, 6. Februar 2012

Spring database transactions before sending JMS

Let's assume you have a Spring-based transactional service doing database operations . Then your transaction is commited the moment your method is finished as Spring surrounds the method with a transaction that it manages for you.

But what if you want to inform other components of the changes you made in the database via JMS? If you send out the JMS message from within the transaction there is no guarantee that the message receiving thread will be able to access the database changes as the transaction might not be committed yet in the sender's thread.

/** Service **/
@Transactional

public void doChangesInDb(){
...
queue.send(message), // inform about DB changes via JMS
...
} // transaction will be committed by Spring

/** MessageListener **/
public void onMessage(JMSMessage message){
dao.getNewOrUpdatedObjects(); // will probably fail
}

The listener code above will probably not work as the database changes might not be committed yet.

How to solve this issue?

The easiest way that I came across was to implement org.springframework.transaction.support.TransactionSynchronization. This class allows you to trigger methods once a transaction is finished. The only thing you have to do is implement a Facade for the JMS Queue that always makes sure that a message is not send out before the transaction is closed. This is how it looks like:

/** EventQueue facade **/
public void put(final Event e) {
if(TransactionSynchronizationManager.isActualTransactionActive()){
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizedSend(jmsTemplate, e));
} else {
jmsTemplate.convertAndSend(e.getTopic().name(), e.getObject());
}
}

The class TransactionSynchronizedSend looks like this:

public class TransactionSynchronizedSend implements TransactionSynchronization, Ordered {

private final JmsTemplate jmsTemplate;
private final Event event;

public TransactionSynchronizedSend(JmsTemplate jmsTemplate, Event e) {
this.jmsTemplate = jmsTemplate;
this.event = e;
}

@Override
public void afterCommit() {
// here we actually send the message
jmsTemplate.convertAndSend(event.getTopic().name(), event.getObject());
}

@Override
public int getOrder() {
// make sure this called last
return 1000;
}
...


Worked like a charm for me and might be helpful to you as well :-)