Java Spring Events: A Developer’s Secret Weapon- Part 2

Sounak De 🧑‍💻
Level Up Coding
Published in
9 min readOct 24, 2023

--

created with love from bingAi

This article is a continuation of my previous piece. If you haven’t read it yet, please do so for a better understanding.

In this second part of Spring events, I will mainly highlight event mechanisms that are based on database operations, their common pitfalls and so on.

Before I start with database-related events, it’s important to understand about database transactions.

Database Transactions — mainly refer to a logical unit of work, that gets completed together or not at all.

Let’s take an example:-

Tony Stark wants to give Black Widow $5. So Bank of America creates a record for this transfer. It first reads the account balance of Tony Stark

($2⁶⁴ 😊), and deducts $5.

Then it reads the account balance of Black Widow and adds $5 to it. All these units of work, like reading the balance, adding and deducting, constitute a whole logical unit of work known as transaction. Say, now before the transaction completes, on a fateful Monday morning, Thanos attacks Earth and the bank servers have a power cut. So if Tony Stark’s account was deducted by $5 before it got credited to Black Widow’s account, it needs to be returned. This reversal is known as rollback.

Photo by Mulyadi on Unsplash

Since now you are equipped with this knowledge of transactions, it’s important to understand how Spring plays an important role in managing this transactional behaviour and how you can leverage them into your project.

Introducing the (drumroll….)

@Transactional annotation

This annotation is mainly used in methods and classes. It works only on the public methods. Behind the scenes, Spring creates a proxy to wrap the method start and end with transaction-related code blocks like transaction.start(), transaction.commit()… etc.

Once you annotate a method with @Transactional, if there is any database operation causing an exception, rollback will occur.

@Transactional annotation can be further configured, with values related to PROPAGATION and ISOLATION. I won’t be covering many of these transactional attributes in this blog. They deserve a blog of their own, as there is so much to explain. However, I will mainly cover how event-based logic during database operation is interconnected with this annotation.

So let’s get our hands dirty…

We will create 2 simple API endpoints to create an Avenger with their special power and read all the Avengers present in the database. Once an avenger is created, we will send them to training by listening to a database event. The Avenger’s data will be stored in an in-memory H2 database.

Photo by Fredrick john on Unsplash

Create a spring boot starter project

Make sure to add these dependencies in the build.gradle / pom.xml

implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'

Add the following properties in the application.properties file

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto = create-drop
spring.jpa.show-sql = true

The names of the properties are self-explanatory. Since it’s an in-memory database, we can keep spring.jpa.hibernate.ddl-auto = create-drop, so that the tables get generated and destroyed on application startup/ termination.

Create an Avenger Entity

@Entity
@Table(name = "AVENGERS")
public class AvengersEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

@Column
private String name;

@Column
private String weapon;

@Column
private boolean isTrainingCompleted;

// Getters, setters
}

@Entity — refers that this class is a data-holding blueprint. It should be mapped with a database table row.

@Table — provides a table name for the entity. If not provided, default values will be applied.

@Id — refers to the Primary key of the table column

@GeneratedValue — refers to the autogeneration of the primary key, following a particular strategy. The strategy can be IDENTITY, SEQUENCE, UUID, TABLE and AUTO. For simplicity’s sake, I have used AUTO, which allows the persistence provider to choose the ID generation strategy according to its recommended specifications, for the particular database.

@Column — maps the field to a column in the table.

Create an Avengers Repository

@Repository
public interface AvengersRepository extends ListCrudRepository<AvengersEntity, Long> {

}

Spring Data provides a very simple mechanism to implement database actions like find(), save() etc. by a simple extend to a Crud Repository.

Create an Avenger Created Event class

public class AvengersCreatedEvent {

private final AvengersEntity avengersEntity;

public AvengersCreatedEvent(AvengersEntity avengersEntity) {
this.avengersEntity = avengersEntity;
}

public AvengersEntity getAvengersEntity() {
return avengersEntity;
}
}

Create an Avengers Service

@Service
public class AvengersService {

private final AvengersRepository avengersRepository;
private final ApplicationEventPublisher applicationEventPublisher;

public AvengersService(AvengersRepository avengersRepository,
ApplicationEventPublisher applicationEventPublisher) {

this.avengersRepository = avengersRepository;
this.applicationEventPublisher = applicationEventPublisher;
}


public AvengersEntity addNewAvengerToTeam(AvengersEntity avengersEntity) {

AvengersEntity avg = this.avengersRepository.save(avengersEntity);
System.out.println("ADDED new avenger: " + avg.getName());
this.applicationEventPublisher.publishEvent(new AvengersCreatedEvent(avg));
return avg;
}

@Transactional(readOnly = true)
public List<AvengersEntity> findAllAvengers() {
return this.avengersRepository.findAll();
}

}

As you might recall from my previous blog, I utilized the ApplicationEventPublisher to publish an event. Now also I have used the same mechanism in the addNewAvengerToTeam() method. This method additionally saves the AvengersEntity by using the repository we created earlier.

Create a simple controller class

@RestController
@RequestMapping("/avengers")
public class AvengersController {

@Autowired
AvengersService avengersService;

@PostMapping
public ResponseEntity<?> createNewAvenger(@RequestBody AvengersEntity avenger) {
AvengersEntity persistedAvenger = this.avengersService.addNewAvengerToTeam(avenger);
return new ResponseEntity<>(persistedAvenger, HttpStatus.CREATED);
}

@GetMapping
public ResponseEntity<?> getAvengers() {
List<AvengersEntity> persistedAvengers = this.avengersService.findAllAvengers();
return new ResponseEntity<>(persistedAvengers, HttpStatus.OK);
}
}

Here I have created a simple controller having two endpoints, one POST and one GET. The POST endpoint is to save Avenger data, while the GET endpoint is to retrieve the list of Avengers currently in the database.

Create an Avenger Joining Event Listener class

@Component
public class AvengersJoiningEventListener {

private AvengersRepository avengersRepository;

public AvengersJoiningEventListener(AvengersRepository avengersRepository) {
this.avengersRepository = avengersRepository;
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendToTraining(AvengersCreatedEvent avengersCreatedEvent) {
avengersCreatedEvent.getAvengersEntity().setTrainingCompleted(true);
this.avengersRepository.save(avengersCreatedEvent.getAvengersEntity());
}
}

Few important things to note over here. The method sendToTraining(…) is annotated with @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT). So what does it mean and what with the phase attribute?

@TransactionalEventListener is an enhanced version of @EventListener that we used before in this example. It was introduced in Spring 4.2, and the speciality of this annotation resides in its ability to trigger a code block, based on database lifecycle operation, or simply called a transaction phase. Four transaction phases can be used.

  • AFTER_COMMIT (default) — called during transaction commit
  • AFTER_ROLLBACK — called during transaction rollback
  • AFTER_COMPLETION — called when a transaction has been completed (be it a successful commit or rollback)
  • BEFORE_COMMIT — called before the transaction commits

In most cases, you will be required to use the AFTER_COMMIT phase and sometimes AFTER_ROLLBACK.

In the code above sendToTraining() method is hooked with the AFTER_COMMIT phase, so when the event producer commit takes place, this listener code gets triggered.

Let’s see this in action –

By using the POST request, an Avenger is created. Ideally, once the above avenger is created, the listener should be called and the trainingCompleted field should be set to true during the GET request.

However, the GET request gives the following response:

Now this is strange!

Photo by Old Youth on Unsplash

Pitfall #1

As you can assume the listener logic is not triggered, hence the value of trainingCompleted is unset. You can write a log to conclude the same. Well, what we missed is annotating the method

addNewAvengerToTeam(…) in Avengers Service with the @Transactional annotation. And why is that important? Because @TransactionalEventListener mentions that it requires an active transaction to get executed. If no transaction is present, it will be simply ignored. So let’s make this change in Avengers service.

  @Transactional
public AvengersEntity addNewAvengerToTeam(AvengersEntity avengersEntity) {
AvengersEntity avg = this.avengersRepository.save(avengersEntity);
System.out.println("ADDED new avenger: " + avg.getName());
this.applicationEventPublisher.publishEvent(new AvengersCreatedEvent(avg));
return avg;
}

Still no luck. The value of trainingCompleted is still unset after this change.

Pitfall #2

Let’s see what the docs say for the AFTER_COMMIT phase:

The transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible. As a consequence, any data access code triggered at this point will still “participate” in the original transaction, allowing to perform some cleanup (with no commit following anymore!), unless it explicitly declares that it needs to run in a separate transaction. Hence: Use PROPAGATION_REQUIRES_NEW for any transactional operation that is called from here.

The solution is simple, we need to create a new transaction, for the listener to execute the save method for persisting the trainingCompleted field to true.

A new transaction can be created using the Propagation.REQUIRES_NEW attribute. The default value for Propagation is REQUIRED.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sentToTraining(AvengersCreatedEvent avengersCreatedEvent) {
avengersCreatedEvent.getAvengersEntity().setTrainingCompleted(true);
this.avengersRepository.save(avengersCreatedEvent.getAvengersEntity());
}

With this change, we can see the proper result in the GET request.

Asynchronous operation

By default, @TransactionalEventListener is synchronous, same as @EventListener. So, like before, we can make the event Listener logic asynchronous by using the @Async annotation.

In our example, say after the Avenger entity is created, it takes 10 sec to complete the training. So let’s modify the code accordingly.

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void sentToTraining(AvengersCreatedEvent avengersCreatedEvent) {
avengersCreatedEvent.getAvengersEntity().setTrainingCompleted(true);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.avengersRepository.save(avengersCreatedEvent.getAvengersEntity());
}

Here you can see that in the AvengersJoiningEventListener class, the method has been modified with an @Async annotation. Also, one extra thing to note over here, the @Transactional(propagation = Propagation.REQUIRES_NEW) is removed.

If you test the code, it should run perfectly. It will take 10 sec to reflect the changes, so do a GET call after 10 seconds to see the change in the trainingCompleted field.

Now, why is the code working even if we remove the Transaction Propagation property? The reason is, by default , transactions in Spring framework are Thread bounded. So, once you get a new Thread, you get a new transaction. Hence no need for Propagation.REQUIRES_NEW.

An Alternative Approach

All the transactional attributes discussed so far, are provided by Spring. JPA lifecycle events offer another option to trigger callbacks based on database operations. There are 7 JPA entity lifecycle event annotations.

  • @PrePersist
  • @PostPersist
  • @PreRemove
  • @PostRemove
  • @PreUpdate
  • @PostUpdate
  • @PostLoad

All the annotations are self-explanatory. The only thing to keep in mind is that the callback methods should return void.

The Avenger entity we created before had an auto-generated ID. Let’s now add a callback method hooked to @PostPersist, to print out the name and ID of that particular Avenger.

Create a new AvengersJoiningJpaEventListener class

@Component
public class AvengersJoiningJpaEventListener {


@PostPersist
private void avengerCreatedCallback(AvengersEntity avenger) {
System.out.println("inside avengerCreatedCallback for avenger: "
+ avenger.getName() + "with id " + avenger.getId());
}
}

Modify the Avengers Entity

@Entity
@Table(name = "AVENGERS")
@EntityListeners(AvengersJoiningJpaEventListener.class)
public class AvengersEntity {

// existing code

}

Here, an @EntityListeners annotation is added to refer to the listener class. Alternatively, the @PostPersist code logic could have been added inside the Avengers entity.

Now if you run the program you can see the desired results.

Photo by krakenimages on Unsplash

Question for the audience:

Suppose a method A is annotated with @Transactional and we want to call another method B inside the same class, from method A . Method B is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW) . Will the @Transactional attributes of method B be honored ?

Enjoyed This Article? Consider Supporting My Work!

Thank you for taking the time to read this article! 😊 I hope you found it informative and insightful. As a developer, understanding the intricacies of Spring events and database transactions can be a game-changer. If you have any questions or feedback, please feel free to leave a comment below.

👏 Please clap for the story if you found this article helpful.

I belong to a non-listed country as a result I can’t earn from Medium. You can tip me here, your contribution is much appreciated.

--

--

Electronics engineer by degree👨‍🎓|| Software engineer by profession 🧑‍💻🧑‍💻. || Java || Spring || Angular || Azure || Microservices