Empowering Data Tracking: A Guide to Spring Boot Auditing with Spring Data JPA

Ebenezer Buabeng
Dev Genius
Published in
6 min readMar 10, 2024

--

Photo by Oskar Kadaksoo on Unsplash

Have you ever encountered a situation where tracing data modifications or identifying the culprit behind a critical change proved challenging? Enterprise applications demand unwavering data security and clear accountability. Auditing, the process of tracking and recording changes to data, plays a crucial role in achieving these goals. This article delves into the world of Spring Boot auditing by exploring the implementation using Spring Data JPA.

How to achieve auditing with Spring Data JPA

  1. Initializing the project. Use the Spring Initializer to set up the application with the following settings.

2. Configure the datasource.

server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/auditdb
username: ebenezer
password: password
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect

3. Defining our Base Auditing Entity.

package com.ebenezer.audit.model;

import jakarta.persistence.*;
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 java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Auditable {

@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;

@CreatedDate
@Column(name = "created_at", updatable = false, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;

@LastModifiedBy
@Column(name = "modified_by")
private String modifiedBy;

@LastModifiedDate
@Column(name = "modified_at")
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;

public String getCreatedBy() {
return createdBy;
}

public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}

public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}

public String getModifiedBy() {
return modifiedBy;
}

public void setModifiedBy(String modifiedBy) {
this.modifiedBy = modifiedBy;
}

public LocalDateTime getModifiedAt() {
return modifiedAt;
}

public void setModifiedAt(LocalDateTime modifiedAt) {
this.modifiedAt = modifiedAt;
}
}

3.1 @MappedSuperclass: This annotation indicates that the class is a superclass for entity classes and should not be mapped directly to a database table.

3.2 @EntityListeners(AuditingEntityListener.class): This annotation is provided by JPA and is used to specify callback listener classes. Spring Data provides its own JPA entity listener class, AuditingEntityListener. This class listens for entity events like Pre-Persist and Pre-Update and automatically populates field annotated with Auditing Metadata.

3.3 @CreatedAt, @CreatedBy, @LastModifiedBy and @LastModifiedAt : The @CreatedBy and @LastModifiedBy is used to capture the user who created or modified the entity whereas @CreatedDate and @LastModifiedDate is used to capture when the change happened. You can use annotations like @CreatedDate and @LastModifiedDate on properties that hold date and time information, eg JDK8 date and time types (LocalDate, LocalDateTime…), long, Long and legacy java Date and Calendar. The AuditingEntityListener class provided by Spring Data will automatically listen for entity events and populate the fields that have these annotations.

4. Defining the Entity which is Auditable.

package com.ebenezer.audit.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

import java.util.UUID;

@Entity
public class Reservation extends Auditable {

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

private String status;

private String location;

public UUID getId() {
return id;
}

public void setId(UUID id) {
this.id = id;
}

public String getStatus() {
return status;
}

public void setStatus(String status) {
this.status = status;
}

public String getLocation() {
return location;
}

public void setLocation(String location) {
this.location = location;
}
}

5. Setting up Repository for Reservations

package com.ebenezer.audit.repository;

import com.ebenezer.audit.exception.GenericNotFoundException;
import com.ebenezer.audit.model.Reservation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.UUID;

@Repository
public interface ReservationRepository extends JpaRepository<Reservation, UUID> {

default Reservation findReservationById(UUID id){
return findById(id)
.orElseThrow(() -> new GenericNotFoundException(String.format("Reservation with id %s not found", id)));
}
}

6. Setting up the Reservation Service

package com.ebenezer.audit.service.impl;

import com.ebenezer.audit.dto.ReservationCreationResultJson;
import com.ebenezer.audit.dto.ReservationInputJson;
import com.ebenezer.audit.dto.ReservationUpdateResultJson;
import com.ebenezer.audit.exception.GenericNotFoundException;
import com.ebenezer.audit.model.Reservation;
import com.ebenezer.audit.repository.ReservationRepository;
import com.ebenezer.audit.service.ReservationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class ReservationServiceImpl implements ReservationService {

private final ObjectMapper mapper;
private final ReservationRepository reservationRepository;

public ReservationServiceImpl(ObjectMapper mapper, ReservationRepository reservationRepository) {
this.mapper = mapper;
this.reservationRepository = reservationRepository;
}

@Override
public ReservationCreationResultJson createReservation(ReservationInputJson reservation) {
Reservation entityReservation = mapper.convertValue(reservation, Reservation.class);
reservationRepository.save(entityReservation);
return mapper.convertValue(entityReservation, ReservationCreationResultJson.class);
}

@Override
public ReservationUpdateResultJson updateReservation(UUID id, ReservationInputJson reservation) {
Reservation entityReservation = reservationRepository.findReservationById(id);
entityReservation.setStatus(reservation.status());
entityReservation.setLocation(reservation.location());
reservationRepository.save(entityReservation);
return mapper.convertValue(entityReservation, ReservationUpdateResultJson.class);
}
}

7. Configuring application to determine who is making creating or modifying entities dynamically.

package com.ebenezer.audit.service.impl;

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

public class AuditorAwareImpl implements AuditorAware<String> {

@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.map(Object::toString);
}
}

Since our application has spring security we can dynamically determine the user who is performing create or modify operations on entities in our application. We need to provide a custom implementation of the AuditorAware interface. This interface defines a method called getCurrentAuditor that returns an optional representing the current auditor (i.e. the currently logged in user); The above implementation uses the spring security’s Authentication object to acquire the user logged in.

8. Setting Up Users for Authentication:

@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password(encoder().encode("password"))
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password(encoder().encode("password"))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}

We have set up two users i.e. “user” and “admin” for our application. We achieve this by configuring an InMemoryUserDetailsManager bean in our application context. This bean provides an in-memory user store with pre-defined user details.

9. Steps to verify our auditing configuration

9.1 Login as admin

Login as admin with credentials “admin” and “password”

9.2 Prepare the JWT of the logged in admin to make the create reservation request

postman screenshot

9.3 Create a Reservation

reservation created

You can see that the createdAt field was automatically populated for us.

9.4 Verify in DB

AuditDB

We can see from the screenshot that the created_by column is automatically set to admin since that is “admin”’s username.

9.5 Let’s log in as User and modify the new created reservation

login as user

9.6 Make a PUT request to modify the created reservation

modified reservation

9.7 Verify in DB

pgadmin screenshot

We can see that the modified_by column is now “user”.

Due to the focused nature of this article on auditing principles and implementations, I’ve provided excerpts of code to illustrate key concepts. However, if readers are interested in exploring the full code implementation, they are welcome to visit my GitHub repository, where they can access the complete codebase.

Conclusion:

In conclusion, auditing plays a vital role in ensuring data integrity, accountability, and compliance within software applications. By implementing auditing, developers can track and monitor changes made to entities, identify potential security risks, and maintain regulatory compliance. I hope this article has provided valuable insights and practical guidance for implementing auditing effectively in your Spring Boot projects. Thank you for joining me on this journey through auditing in Spring Boot.

--

--