I'm using Spring application events within my service layer to notify the wider system when specific events occur. The issue I have is that the events are fired within a transactional context (I'm using Spring's @Transactional
annotation). The danger is that I fire an event and then the transaction fails on commit (can happen when using, for example, Hibernate). In this scenario, the event listeners will be assuming some state that will then be rolled back after they've executed. The danger here is that I may, for example, have sent an email to confirm a user's registration on a website when, in fact, their user account was not actually created.
Is there a way, preserving the use of annotations, to somewhere flag an event to be fired after the transaction commits? It kind of seems somewhat analogous to using SwingUtilities.invokeLater(Runnable runnable)
when doing GUI programming in Java Swing. I want to say, execute this bit of code later on once the current transaction开发者_运维问答 commits successfully.
Any ideas?
Thanks,
Andrew
This works for me:
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// things to do when commited
}
// other methods to override are available too
});
You can make use of TransactionSynchronizationManager without needing to hack PlatformTransactionManager.
Note: TransactionAware is a marker interface indicating that a ApplicationListener wants to receive a Event after transaction is committed successfully.
public class TransactionAwareApplicationEventMulticaster extends SimpleApplicationEventMulticaster {
@Override
public void multicastEvent(ApplicationEvent event) {
for (ApplicationListener listener : getApplicationListeners(event)) {
if ((listener instanceof TransactionAware) && TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(
new EventTransactionSynchronization(listener, event));
}
else {
notifyEvent(listener, event);
}
}
}
void notifyEvent(final ApplicationListener listener, final ApplicationEvent event) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
public void run() {
listener.onApplicationEvent(event);
}
});
}
else {
listener.onApplicationEvent(event);
}
}
class EventTransactionSynchronization extends TransactionSynchronizationAdapter {
private final ApplicationListener listener;
private final ApplicationEvent event;
EventTransactionSynchronization(ApplicationListener listener, ApplicationEvent event) {
this.listener = listener;
this.event = event;
}
@Override
public void afterCompletion(int status) {
if ((phase == TransactionPhase.AFTER_SUCCESS)
&& (status == TransactionSynchronization.STATUS_COMMITTED)) {
notifyEvent(listener, event);
}
}
}
}
The proper way to solve your problem with confirmation emails is probably to use a distributed transactions with a JMS resource participating in them.
However, as a quick-and-dirty solution, you may try to create a wrapper around a PlatformTransactionManager
which will execute Runnable
s registered in some ThreadLocal
storage after successfull commit (and remove it from ThreadLocal
after rollback). Actually, AbstractPlatformTransactionManager
have exactly this kind of logic with TransactionSynchronization
s, but it's buried too deep into its implementation details.
精彩评论