Skip to main content

JPA Auditing: Persisting Audit Logs Automatically using EntityListeners

In my previous article Spring Data JPA Auditing: Saving CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate automatically, I have discussed why Auditing is important for any business application and how we can use Spring Data JPA automate it.

I have also discussed how Spring Data uses JPA’s EntityListeners and callback methods to automatically update CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate properties.

Well, here in this article I am going dig a little bit more and discuss how we can use JPA EntityListeners to create audit logs and keep information of every insert, update and delete operation on our data.

I will take the File entity example from the previous article and walk you through the necessary steps and code portions you will need to include in our project to automate the Auditing process.

We will use Spring Boot, Spring Data JPA (Because it gives us complete JPA functionality plus some nice customization by Spring), MySql to demonstrate this.

We will need to add below parent and dependencies to our pom file

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

Implementing JPA Callback Methods using annotations @PrePersist, @PreUpdate, @PreRemove


JPA provides us the functionality to define callback methods for any entity using annotations @PrePersist, @PreUpdate, @PreRemove and these methods will get invoked before their respective life cycle event.

Similar to pre-annotations, JPA also provides post annotations like @PostPersist, @PostUpdate, @PostRemove, and @PostLoad. We can use them to define callback methods which will get triggered after the event.

JPA-Automatic-Auditing-Saving-Audit-Logs

Name of the annotation can tell you their respective event e.g @PrePersist - Before entity persists and @PostUpdate - After entity gets updated and this is same for other annotations as well.

Defining callback methods inside entity


We can define callback methods inside our entity class but we need to follow some rules like internal callback methods should always return void and take no argument. They can have any name and any access level and can also be static.

@Entity
public class File {

@PrePersist
public void prePersist() { // Persistence logic }

@PreUpdate
public void preUpdate() { //Updation logic }

@PreRemove
public void preRemove() { //Removal logic }

}

Defining callback methods in an external class and use @EntityListeners


We can also define our callback methods in an external listener class in a manner that they should always return void and accepts target object as the argument. However, they can have any name and any access level and can also be static.

public class FileEntityListener {
@PrePersist
public void prePersist(File target) { // Persistence logic }

@PreUpdate
public void preUpdate(File target) { //Updation logic }

@PreRemove
public void preRemove(File target) { //Removal logic }
}


And we will need to register this FileEntityListener class on File entity or its superclass by using @EntityListeners annotation

@Entity
@EntityListeners(FileEntityListener.class)
class File extends Auditable<String> {

@Id
@GeneratedValue
private Integer id;
private String name;
private String content;

// Fields, Getters and Setters
}

Advantages of using @EntityListeners


  • First of all, We should not write any kind of business logic in our entity classes and follow Single Responsibility Principle. Every entity class should be POJO (Plain Old Java Object).
  • We can have only one callback method for a particular event in a single class e.g. only one callback method with @PrePresist is allowed in a class. While we can define more than one listener class in @EntityListeners and every listener class can have a @PrePersist.

For example, I have used @EntityListeners on File and provided FileEntityListener class to it and I have also extended an Auditable class in File class.

The Auditable class itself have a @EntityListeners on it with AuditingEntityListener class because I am using this class to persist createdBy and other above-mentioned properties, You can check my previous article Spring Data JPA Auditing: Saving CreatedBy, CreatedDate, LastModifiedBy, LastModifiedDate automatically for more details.

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Auditable<U> {

@CreatedBy
protected U createdBy;

@CreatedDate
@Temporal(TIMESTAMP)
protected Date createdDate;

@LastModifiedBy
protected U lastModifiedBy;

@LastModifiedDate
@Temporal(TIMESTAMP)
protected Date lastModifiedDate;

// Getters and Setters
}

We will also need to provide getters, setters, constructors, toString and equals methods to all the entities. However, you may like to look Project Lombok: The Boilerplate Code Extractor if you want to auto-generate these things.

Now we are all set and we need to implement our logging strategy, we can store history logs of the File in a separate history table FileHistory.

@Entity
@EntityListeners(AuditingEntityListener.class)
public class FileHistory {

@Id
@GeneratedValue
private Integer id;

@ManyToOne
@JoinColumn(name = "file_id", foreignKey = @ForeignKey(name = "FK_file_history_file"))
private File file;

private String fileContent;

@CreatedBy
private String modifiedBy;

@CreatedDate
@Temporal(TIMESTAMP)
private Date modifiedDate;

@Enumerated(STRING)
private Action action;

public FileHistory() {
}

public FileHistory(File file, Action action) {
this.file = file;
this.fileContent = file.toString();
this.action = action;
}

// Getters, Setters
}

Here Action is an enum

public enum Action {

INSERTED("INSERTED"),
UPDATED("UPDATED"),
DELETED("DELETED");

private final String name;

private Action(String value) {
this.name = value;
}

public String value() {
return this.name;
}

@Override
public String toString() {
return name;
}
}

And we will need to insert an entry in FileHistory for every insert, update, delete operation and we need to write that logic inside our FileEntityListener class. For this purpose, we will need to inject either repository class or EntityManager in FileEntityListener class.

Injecting Spring Managed Beans like EntityManager in EntityListeners


But here we have a problem, EntityListeners are instantiated by JPA not Spring, So Spring cannot inject any Spring-managed bean e.g. EntityManager in any EntityListeners.

So if you try to auto-wire EntityManager inside FileEntityListener class, it will not work

@Autowired EntityManager entityManager; //Will not work and entityManager will be null always

I have also written a separate article on how to AutoWire Spring Beans Into Classes Not Managed By Spring Like JPA Entity Listeners, you can read it if you want to know more.

And I am using the same idea here to make it work, we will create a utility class to fetch Spring managed beans for us

@Service
public class BeanUtil implements ApplicationContextAware {

private static ApplicationContext context;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}

public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}

}

And now we will write history record creation logic inside FileEntityListener

public class FileEntityListener {

@PrePersist
public void prePersist(File target) {
perform(target, INSERTED);
}

@PreUpdate
public void preUpdate(File target) {
perform(target, UPDATED);
}

@PreRemove
public void preRemove(File target) {
perform(target, DELETED);
}

@Transactional(MANDATORY)
private void perform(File target, Action action) {
EntityManager entityManager = BeanUtil.getBean(EntityManager.class);
entityManager.persist(new FileHistory(target, action));
}

}

And now if we will try to persist or update and file object these auditing properties will automatically get saved.

You can find complete code on this Github Repository and please feel free to provide your valuable feedback.

Comments

  1. Hello!! Excelent post... What if a do not want to save auditing information into a table? but in a log file... how can i do that? I need to save into a file, the logs of who attempst to update, save, delete, etc an entity... i think this example helpme but a get confused...

    ReplyDelete
  2. You just need to change the logic of `perform` method inside `FileEntityListener ` class. As of now that method is inserting the records into the database you can change it save the records into a log file.

    ReplyDelete

Post a Comment

Popular posts from this blog

Java Cloning - Copy Constructor versus Cloning

In my previous article Java Cloning and Types of Cloning (Shallow and Deep) in Details with Example , I have discussed Java Cloning in details and answered questions about how we can use cloning to copy objects in Java, what are two different types of cloning (Shallow & Deep) and how we can implement both of them, if you haven’t read it please go ahead. In order to implement cloning, we need configure our classes to follow below steps Implement Cloneable interface in our class or its superclass or interface, Define clone() method which should handle CloneNotSupportedException (either throw or log), And in most cases from our clone() method we call the clone() method of the superclass. And super.clone() will call its super.clone() and chain will continue until call will reach to clone() method of the Object class which will create a field by field mem copy of our object and return it back. Like everything Cloning also comes with its advantages and disadvantages. However, Java c...

Creating objects through Reflection in Java with Example

In Java, we generally create objects using the new keyword or we use some DI framework e.g. Spring to create an object which internally use Java Reflection API to do so. In this Article, we are going to study the reflective ways to create objects. There are two methods present in Reflection API which we can use to create objects Class.newInstance() → Inside java.lang package Constructor.newInstance() → Inside java.lang.reflect package However there are total 5 ways create objects in Java, if you are not aware of them please go through this article 5 Different ways to create objects in Java with Example . Both Class.newInstance() and java.lang.reflect.Constructor.newInstance() are known as reflective methods because these two uses reflection API to create the object. Both are not static and we can call earlier one on a class level object while latter one needs constructor level object which we can get by using the class level object. Class.newInstance() The Class class is th...

Everything About Object Oriented JavaScript

Complete explanation of Object Oriented JavaScript 01:50  JavaScript Objects 02:36  Objects in Objects 04:12  Constructor Functions 05:58  instanceof 06:28  Passing Objects to Functions 08:09  Prototypes 09:34  Adding Properties to Objects 10:44  List Properties in Objects 11:38  hasOwnProperty 12:42  Add Properties to Built in Objects 14:31  Private Properties 18:01  Getters / Setters 21:20  defineGetter / defineSetter 24:38  defineProperty 27:07  Constructor Function Getters / Setters 29:40  Inheritance 37:13  Intermediate Function Inheritance 39:14  Call Parent Functions 41:51  ECMAScript 6 47:31  Singleton Pattern 49:32  Factory Pattern 52:53  Decorator Pattern 54:52  Observer Pattern