This article mainly explains how TransactionalEventListener work? Suitable in what scenario, can solve what problems? And how it differs from EventListener.
The sample
For an example of a business scenario, let’s say we have a requirement and send an email to the user after the user is successfully created. There are two things to do here:
- Create a user
- Send emails to users
For this requirement, we might have the following implementation without thinking.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public User() {}
...
//getters
//equals and hashcode
}
Copy the code
Create a Repository for the User
public interface UserRepository extends JpaRepository<User, Long> {} @Service public class EmailService{ @Transactional public void sendEmail(String email) { //send email } } @Service public class UserService { private final EmailService emailService; private final UserRepository userRepository; public UserService(EmailService emailService, UserRepository userRepository) { this.emailService = emailService; this.userRepository = userRepository; } @Transactional public User createUser(User user) { User newUser = userRepository.save(user); emailService.sendEmail(user.getEmail()); return newUser; }}Copy the code
The above implementation is the easiest to implement, but this implementation is problematic. Let’s think about it. The core of this function is to create a user, and sending an email is a side effect (sending an email does not affect the creation of a user). What’s the problem if you put these two operations in one transaction? Obviously, if an exception is thrown when the user is created, the transaction is rolled back, and the method exits prematurely, then no mail will be sent, which is normal. But the following two scenarios are unacceptable:
- If the email fails to be sent, the transaction is rolled back and the user creation fails.
- If the transaction fails to commit after the email is sent, the user receives the email but fails to create the user.
Although these situations occur in small probability, but as the requirements of their own programming ape, this is not tolerated, we are responsible for the business we write.
Ok, let’s do a refactoring of the above implementation, separating the business code that creates the user and sends the mail directly and decoupling the implementation using Spring Application Events.
The modified Service looks like this
@Service public class CustomerService { private final UserRepository userRepository; private final ApplicationEventPublisher applicationEventPublisher; public CustomerService(UserRepository userRepository, ApplicationEventPublisher applicationEventPublisher) { this.userRepository = userRepository; this.applicationEventPublisher = applicationEventPublisher; } @Transactional public Customer createCustomer(User user) { User newUser = userRepository.save(user); final UserCreatedEvent event = new UserCreatedEvent(newUser); applicationEventPublisher.publishEvent(event); return newUser; }}Copy the code
From the above code, we know that UserService depends on two Beans:
-
UserRepository – Does persistence work
-
Internal events ApplicationEventPublisher – send Spring
public class UserCreatedEvent {
private final User user;
public UserCreatedEvent(User user) { this.user = user; }
public User getUser() { return user; }
. //equals and hashCode }
Note that this class is just a simple POJO object. Since Spring 4.2, we can publish any object without inheritingApplicationEvent. Spring wraps them as PayloadApplicationEvent.
We need an Event Listener to handle the above events.
@Component public class UserCreatedEventListener { private final EmailService emailService; public UserCreatedEventListener(EmailService emailService) { this.emailService = emailService; } @EventListener public void processUserCreatedEvent(UserCreatedEvent event) { emailService.sendEmail(event.getUser().getEmail()); }}Copy the code
With the above refactoring, we separated the business code for creating users and sending emails, but did we solve the problems mentioned above? The answer is no, even though we decouple the business code using EventListener, the underlying two functions are still executed in the same transaction. (One might wonder if it’s ok to add @async to the Listener method for asynchronous execution. Of course not, the email must be sent after the user has been created, there are business dependencies), meaning that both of the above situations will still happen. So the question is, is there a solution?
Of course, is to use @ TransactionalEventListener replace @ EventListener, the result is send an email after the create user and commit the transaction.
TransactionalEventListener
TransactionalEventListener is on the increase of the EventListener be annotated methods in different stages of the transaction to trigger the execution, if the event is not released in the active transaction, unless explicitly set the fallbackExecution () flag is true, Otherwise, the event will be discarded; If a transaction is running, the event is handled according to its TransactionPhase.
Notice: You can Order all listeners by annotating @order to ensure that they are executed in the desired Order.
Let’s first look at what TransactionPhase has:
- AFTER_COMMIT – Default setting, executed after a transaction commits
- AFTER_ROLLBACK – Executes after the transaction rollback
- AFTER_COMPLETION – To execute after a transaction has completed (successful or not)
- BEFORE_COMMIT – Executed before a transaction commits
The transformed Listener looks like this
@Component public class UserCreatedEventListener { private final EmailService emailService; public UserCreatedEventListener(EmailService emailService) { this.emailService = emailService; } @TransactionalEventListener public void processUserCreatedEvent(UserCreatedEvent event) { emailService.sendEmail(event.getUser().getEmail()); }}Copy the code
Ok, now we can make sure that our business is running normally and that the creation of users is not affected by sending emails. Let’s dig it, see TransactionalEventListener did it.
The principle of analysis
Spring processing source code is given below, you will be at a glance:
@Override public void onApplicationEvent(ApplicationEvent event) { if (TransactionSynchronizationManager.isSynchronizationActive() && TransactionSynchronizationManager.isActualTransactionActive()) { TransactionSynchronization transactionSynchronization = createTransactionSynchronization(event); TransactionSynchronizationManager.registerSynchronization(transactionSynchronization); } else if (this.annotation.fallbackExecution()) { if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) { logger.warn("Processing " + event + " as a fallback execution on AFTER_ROLLBACK phase"); } processEvent(event); } else { // No transactional event execution at all if (logger.isDebugEnabled()) { logger.debug("No transaction is active - skipping " + event); }}}Copy the code
Explain the above code:
- If the current in the activation of the transaction, then creates a TransactionSynchronization, and put it in a collection. It means it’s not executed, it’s just stored temporarily.
- If there is no transaction and fallbackExecution is explicitly set to true, it is executed directly, and the effect is the same as EventListener.
- If there is no transaction and fallbackExecution is false, the Event is discarded without processing.
Since TransactionSynchronization are stored up, so what’s the time to trigger the execution?
AFTER_COMMIT is an example of AFTER_COMMIT (the other phases are similar). Look at this code:
AbstractPlatformTransactionManager class
private void processCommit(DefaultTransactionStatus status) throws TransactionException {
try {
boolean beforeCompletionInvoked = false;
try {
boolean unexpectedRollback = false;
prepareForCommit(status);
triggerBeforeCommit(status);
triggerBeforeCompletion(status);
beforeCompletionInvoked = true;
if (status.hasSavepoint()) {
if (status.isDebug()) {
logger.debug("Releasing transaction savepoint");
}
unexpectedRollback = status.isGlobalRollbackOnly();
status.releaseHeldSavepoint();
}
else if (status.isNewTransaction()) {
if (status.isDebug()) {
logger.debug("Initiating transaction commit");
}
unexpectedRollback = status.isGlobalRollbackOnly();
doCommit(status);
}
else if (isFailEarlyOnGlobalRollbackOnly()) {
unexpectedRollback = status.isGlobalRollbackOnly();
}
// Throw UnexpectedRollbackException if we have a global rollback-only
// marker but still didn't get a corresponding exception from commit.
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction silently rolled back because it has been marked as rollback-only");
}
}
catch (UnexpectedRollbackException ex) {
// can only be caused by doCommit
triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);
throw ex;
}
catch (TransactionException ex) {
// can only be caused by doCommit
if (isRollbackOnCommitFailure()) {
doRollbackOnCommitException(status, ex);
}
else {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
}
throw ex;
}
catch (RuntimeException | Error ex) {
if (!beforeCompletionInvoked) {
triggerBeforeCompletion(status);
}
doRollbackOnCommitException(status, ex);
throw ex;
}
// Trigger afterCommit callbacks, with an exception thrown there
// propagated to callers but the transaction still considered as committed.
try {
triggerAfterCommit(status);
}
finally {
triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED);
}
}
finally {
cleanupAfterCompletion(status);
}
}
Copy the code
Then look at the implementation of triggerAfterCommit
/** * Trigger {@code afterCommit} callbacks. * @param status object representing the transaction */ private void triggerAfterCommit(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { if (status.isDebug()) { logger.trace("Triggering afterCommit synchronization"); } TransactionSynchronizationUtils.triggerAfterCommit(); }}Copy the code
Call the TransactionSynchronizationUtils triggerAfterCommit method here, continue to follow
public static void triggerAfterCommit() { invokeAfterCommit(TransactionSynchronizationManager.getSynchronizations()); } public static void invokeAfterCommit(@Nullable List<TransactionSynchronization> synchronizations) { if (synchronizations ! = null) { for (TransactionSynchronization synchronization : synchronizations) { synchronization.afterCommit(); }}}Copy the code
See, first get all TransactionSynchronization, then call their afterCommit method, will really start to handle the Event.
conclusion
Now we make A summary, if you encounter such business, operating B need after A transaction commit operation to perform, so TransactionalEventListener is A good choice. One important point to note here is that if an operation B has data changes and persists, and you want to execute them in the AFTER_COMMIT phase of an operation A, you need to declare the B transaction as PROPAGATION_REQUIRES_NEW. If operation B uses the default PROPAGATION_REQUIRED, it will be added directly to operation A’s transaction, but transaction A will not commit. As A result, the program writes modification and save logic. The database data, however, did not change, and the solution was to explicitly set the transaction for operation B to PROPAGATION_REQUIRES_NEW.
Note:
All see here, if feel helpful, also ask you to give me a small encouragement, move a finger, help point a thumbs-up! Thank you, if you think there is any mistake or different from your cognition, please leave a comment in the comment section, I will discuss and correct it in the first time!!