Overview
In this tutorial, I will show you how to implement auditing in your application. Some aspects of the implementation, which are prerequisites for executing this tutorial, are beyond its scope but will be covered at a later stage.
Tutorial Overview
This tutorial explores the process of auditing an application, focusing on identifying the creator of a record, the creation timestamp, and the originating IP address. Additionally, if modifications have been made, it will detail the user involved, the modification timestamp, and the corresponding IP address. In the concluding section, I will introduce how the API Javers functions, providing a comprehensive snapshot of all records created and modified. This feature is invaluable when you need to revert to or review the original state of a record.
Project Setup
To set up the project, check out the demo project and run the following commands:
git clone git@github.com:softwarebuilding-io/application-auditing.git
After cloning the application, run the following command to create the tables:
mvn clean compile -P docker
Import your application into your favorite IDE, then run the application.

After the application starts, please check the following URL: http://localhost:8080/fusion-plex/
. The login details are:
- Username: user
- Password: User@1234
Step 1: Updating the SQL Script
We will start by updating the SQL script that creates the tables. This script will include the following columns in the genre
and title
tables:
created_ip
: IP of the user who created the recordcreated_on
: Timestamp when the record was createdcreated_user
: Username of the person who created the recordupdated_ip
: IP of the user who updated the recordupdated_on
: Timestamp when the record was updatedupdated_user
: Username of the person who updated the record
Open the create-tables.sql
file and update it as follows:
CREATE TABLE public.genre
(
id UUID NOT NULL DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL UNIQUE,
created_ip VARCHAR(15) NOT NULL DEFAULT '127.0.0.1',
created_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_user VARCHAR(60) NOT NULL DEFAULT 'ADMIN',
updated_ip VARCHAR(15),
updated_on TIMESTAMP,
updated_user VARCHAR(60),
PRIMARY KEY (id)
);
CREATE TABLE public.title
(
id UUID NOT NULL DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL UNIQUE,
description VARCHAR(255) NOT NULL,
release_year SMALLINT NOT NULL,
type INTEGER NOT NULL,
created_ip VARCHAR(15) NOT NULL DEFAULT '127.0.0.1',
created_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_user VARCHAR(60) NOT NULL DEFAULT 'ADMIN',
updated_ip VARCHAR(15),
updated_on TIMESTAMP,
updated_user VARCHAR(60),
PRIMARY KEY (id)
);
Finally, run the following command to update the database with the new columns:
mvn clean compile -P docker
Step 2: Base Entity
Next, we will update the Base Entity and add the following attributes and methods. All concrete entities will inherit from the Base Entity, which is a best practice to avoid recreating the same functionality throughout the application.
package io.softwarebuilding.fusionplex.entity;
import io.softwarebuilding.fusionplex.util.IpUtil;
import io.softwarebuilding.fusionplex.util.LoginUtil;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.time.ZonedDateTime;
@MappedSuperclass
public abstract class BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = -33114089982657335L;
@Column(name = "created_on", nullable = false)
private ZonedDateTime createdOn;
@Column(name = "created_ip", nullable = false, length = 15)
private String createdIp;
@Column(name = "created_user", nullable = false, length = 60)
private String createdUser;
@Column(name = "updated_on")
private ZonedDateTime updatedOn;
@Column(name = "updated_ip", length = 15)
private String updatedIp;
@Column(name = "updated_user", length = 60)
private String updatedUser;
// Getter and setter methods for each field
/**
* PrePersist method for auditing purposes.
* Populates created_* fields before a new record is persisted.
*/
@PrePersist
public void prePersist() {
this.createdUser = getAuthenticatedUser();
this.createdOn = ZonedDateTime.now();
this.createdIp = getClientIpAddress();
}
/**
* PreUpdate method for auditing purposes.
* Populates updated_* fields before an existing record is updated.
*/
@PreUpdate
public void preUpdate() {
this.updatedUser = getAuthenticatedUser();
this.updatedOn = ZonedDateTime.now();
this.updatedIp = getClientIpAddress();
}
private String getAuthenticatedUser() {
return LoginUtil.getAuthenticatedUser();
}
private String getClientIpAddress() {
return IpUtil.getClientIpAddress();
}
}
In this section, we have streamlined the SQL script and Java code to clearly align the database schema with the application’s auditing logic.
Step 3: Adding Javers Dependency
Javers is an open-source library used for auditing your application. It provides a comprehensive view of the entire lifecycle of a record, tracking initial changes and maintaining snapshots of all modifications. This allows you to revert to the very first version of a record. To include Javers in your project, add the following dependency to your pom.xml
file:
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-spring-boot-starter-sql</artifactId>
<version>7.3.8</version>
</dependency>
Step 4: Creating the Audit Layers
We will begin by creating the AuditService
layer, which will interact with the Javers API. Note that there is no need for a repository layer because Javers directly interacts with the database.
package io.softwarebuilding.fusionplex.service;
import io.softwarebuilding.fusionplex.entity.Genre;
import io.softwarebuilding.fusionplex.entity.Title;
import org.javers.core.Javers;
import org.javers.core.diff.Change;
import org.javers.repository.jql.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AuditService {
private final Javers javers;
@Autowired
public AuditService(Javers javers) {
this.javers = javers;
}
public String getGenresChanges() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Genre.class);
List<Change> changes = this.javers.findChanges(jqlQuery.build());
return this.javers.getJsonConverter().toJson(changes);
}
public String getTitleChanges() {
QueryBuilder jqlQuery = QueryBuilder.byClass(Title.class);
List<Change> changes = this.javers.findChanges(jqlQuery.build());
return this.javers.getJsonConverter().toJson(changes);
}
}
Next, we will implement the AuditController
layer, which will display all the audit data on the screen.
package io.softwarebuilding.fusionplex.controller;
import io.softwarebuilding.fusionplex.service.AuditService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AuditController {
private final AuditService auditService;
@Autowired
public AuditController(AuditService auditService) {
this.auditService = auditService;
}
@GetMapping("/admin/audit/genres")
public String showAuditGenresPage(Model model) {
String changes = this.auditService.getGenresChanges();
model.addAttribute("changes", changes);
return "auditGenre";
}
@GetMapping("/admin/audit/titles")
public String showAuditTitlesPage(Model model) {
String changes = this.auditService.getTitleChanges();
model.addAttribute("changes", changes);
return "auditTitle";
}
}
Next, we will create the auditGenre.html
and auditTitle.html
files, which are the actual UIs to display to the user. These files include placeholders for your HTML code, so ensure you replace them with actual content and path references.
Step 5: Adding Javers Annotation
Finally, to enable Javers auditing for your entities, add the @JaversSpringDataAuditable
annotation to all the repositories you want audited. Below are examples for Genre
and Title
entities:
package io.softwarebuilding.fusionplex.repository;
import io.softwarebuilding.fusionplex.entity.Genre;
import org.javers.spring.annotation.JaversSpringDataAuditable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
@JaversSpringDataAuditable
public interface GenreRepository extends JpaRepository<Genre, UUID> {
Optional<Genre> findByName(String name);
}
package io.softwarebuilding.fusionplex.repository;
import io.softwarebuilding.fusionplex.entity.Title;
import org.javers.spring.annotation.JaversSpringDataAuditable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.UUID;
@JaversSpringDataAuditable
public interface TitleRepository extends JpaRepository<Title, UUID> {
}
With these annotations in place, both Genre
and Title
entities will have all their records audited.
Conclusion
Congratulations on completing this tutorial on implementing auditing in your application using Javers. You’ve learned how to integrate Javers into your Spring Boot application, manage changes with service and controller layers, and ensure changes to your entities are auditable.
Auditing is essential for maintaining data integrity and transparency, enhancing security, and complying with data governance standards. This tutorial has equipped you with the tools to provide detailed insights into the lifecycle of records.
Feel free to adapt and expand these concepts for your specific project needs. Refer back to this guide whenever you need a refresher on auditing with Javers.
Happy coding!