Skip to content

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.

ProductEventHandler
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
}

Comments