Listening to events in SpringBoot¶
Jotting down some examples of different ways to handle events in Spring Boot
Entity Lifecycle¶
Entities, i.e. pojos annotated with @Entity
, can utilize the following methods with annotations to manage life cycle events of the entity. JPA defines several lifecycle events that occur during an entity's persistence operations. You can use annotations to specify methods that should be executed when these events occur.
@PrePersist
: Called before the entity is persisted (inserted into the database).@PostPersist
: Called after the entity is persisted.@PreUpdate
: Called before the entity is updated in the database.@PostUpdate
: Called after the entity is updated.@PreRemove
: Called before the entity is removed from the database.@PostRemove
: Called after the entity is removed.@PostLoad
: Called after the entity is loaded from the database.
@Entity
public class MyEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ... other fields
@PrePersist
public void beforePersist() {
System.out.println("About to persist MyEntity with ID: " + this.id);
// Custom logic here (e.g., setting default values)
}
@PostPersist
public void afterPersist() {
System.out.println("MyEntity persisted with ID: " + this.id);
}
@PreUpdate
public void beforeUpdate() {
System.out.println("About to update MyEntity with ID: " + this.id);
}
@PostUpdate
public void afterUpdate() {
System.out.println("MyEntity updated with ID: " + this.id);
}
// ... other lifecycle methods
}
This can also be separated out into a separate class for separation of concerns
import jakarta.persistence.*;
public class MyEntityListener {
@PrePersist
public void beforePersist(MyEntity entity) { // Note the entity parameter
System.out.println("Listener: About to persist MyEntity with ID: " + entity.getId());
}
@PostPersist
public void afterPersist(MyEntity entity) {
System.out.println("Listener: MyEntity persisted with ID: " + entity.getId());
}
// ... other lifecycle methods
}
@Entity
@EntityListeners(MyEntityListener.class) // Associate the listener with the entity
public class MyEntity {
// ... fields
}
AuditingEntityListener¶
SpringBoot also has an AuditingEntityListener
class out of the box. Note the annodations below
@CreatedBy
@LastModifiedBy
@CreatedDate
@LastModifiedDate
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
@CreatedDate
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime updatedDate;
// ... other fields
}
Now create an implementation of AuditorAware
so that Spring knows who the auditor (user) is.
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("system");
}
return Optional.ofNullable(authentication.getName());
}
}
What if your auditor is not a string value, but an Object like a User
entity.
@Entity
@EntityListeners(AuditingEntityListener.class) // Important: Enable auditing for this entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne // Establish the relationship
@JoinColumn(name = "created_by_user_id") // Specify the foreign key column
@CreatedBy // Mark this field for auditing
private User createdBy;
@ManyToOne
@JoinColumn(name = "updated_by_user_id")
@LastModifiedBy
private User updatedBy;
Now you'll simply create an implementation of the typed AuditorAware<User>
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SpringSecurityAuditorAware implements AuditorAware<User> {
private final UserRepository userRepository;
public SpringSecurityAuditorAware(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Optional<User> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty(); // Or handle as appropriate for your application
}
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
return userRepository.findByUsername(username); // Returns Optional<User>
}
return Optional.empty();
}
}
Inheritance¶
Now, let's say you want to have those 4 auditing columns but don't want to declare them in every class. Well, you could create a @MappedSuperClass
class.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable<U> {
@Column(name = "created_by")
private U createdBy;
@Column(name = "updated_by")
private U updatedBy;
@Column(name = "created_date")
private LocalDateTime createdDate;
@Column(name = "updated_date")
private LocalDateTime updatedDate;
// Getters and setters
Every entity that inherits from this Auditable will now have those 4 columns out of the box.
@Entity
@Table(name = "products")
public class Product extends Auditable<String> {
// ... your entity fields
}
RespositoryEventHandler¶
Instead of listening to entity lifecycle hooks, you can also register event handlers directly to repository actions using @RepositoryEventHandler
.
import org.springframework.data.rest.core.annotation.HandleAfterCreate;
import org.springframework.data.rest.core.annotation.HandleAfterDelete;
import org.springframework.data.rest.core.annotation.HandleBeforeCreate;
import org.springframework.data.rest.core.annotation.HandleBeforeDelete;
import org.springframework.data.rest.core.annotation.RepositoryEventHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
@RepositoryEventHandler
public class ProductEventHandler {
@HandleBeforeCreate
public void handleProductBeforeCreate(Product product) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
String username = authentication.getName();
product.setCreatedBy(username);
product.setCreatedDate(LocalDateTime.now());
product.setUpdatedBy(username);
product.setUpdatedDate(LocalDateTime.now());
}
}
@HandleBeforeDelete
public void handleProductBeforeDelete(Product product) {
// Handle audit logic for delete operations if needed
}
@HandleAfterCreate
public void handleProductAfterCreate(Product product) {
//Handle post create operations if needed.
}
@HandleAfterDelete
public void handleProductAfterDelete(Product product) {
//Handle post delete operations if needed.
}
}
Whether to use @EntityListeners
or @RepositoryEventHandler
might depend on the specific action needing to take place or the preference of the developer.
Aspect¶
Here is an example of an Aspect
in which we audit every repository save method and add an entry to a table
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private AuditLogRepository auditLogRepository; // Your audit log repository
@Autowired
private AuditingService auditingService;
@AfterReturning(pointcut = "execution(* com.example.repository.*.save(..)) || execution(* com.example.repository.*.delete(..))", returning = "result")
public void logEntityChanges(JoinPoint joinPoint, Object result) {
Object entity = joinPoint.getArgs()[0]; // Get the entity
String operation = joinPoint.getSignature().getName().contains("delete") ? "DELETE" : "SAVE";
String entityType = entity.getClass().getSimpleName();
User currentUser = auditingService.getCurrentUser();
AuditLogEntry logEntry = new AuditLogEntry();
logEntry.setEntityType(entityType);
logEntry.setOperation(operation);
logEntry.setTimestamp(LocalDateTime.now());
if (currentUser != null) {
logEntry.setUserId(currentUser.getId());
}
auditLogRepository.save(logEntry);
}
}
//AuditLogEntry.java
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
public class AuditLogEntry {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String entityType;
private String operation;
private LocalDateTime timestamp;
private Long userId;
//getters and setters
}