When we hear the word auditing, the first thing that comes to mind is an audit log that contains each version of the audited entity. Implementing an audit log is a complex task that takes a lot of time. Luckily, most of the time we don’t need to do it.
However, it is quite common that we must be able to answer to the following questions:
- When the entity X was created and/or modified?
- Who created and/or modified the entity X?
The previous part of this tutorial described how we can use the auditing infrastructure of Spring Data JPA for finding the answer to the first question.
This blog posts describes how we can find the answer to the second question. We will modify our example application to store the username of the authenticated user who created a new todo entry and updated the information of an existing todo entry.
Let’s start by creating a component that returns the information of the authenticated user.
Additional Reading:
If you are not familiar with Spring Data JPA, you should read the following blog posts before you continue reading this blog post:
- Spring Data JPA Tutorial: Introduction provides a quick introduction to Spring Data JPA and gives an overview of the Spring Data repository interfaces.
- Spring Data JPA Tutorial: Getting the Required Dependencies describes how you can get the required dependencies.
- Spring Data JPA Tutorial: Configuration describes how you can configure the persistence layer of a Spring application that uses Spring Data JPA.
- Spring Data JPA Tutorial: Auditing, Part One describes how you can add the creation and modification time fields into your entities by using the auditing infrastructure of Spring Data JPA.
Getting the Information of the Authenticated User
The auditing infrastructure of Spring Data JPA uses the AuditorAware<T> interface when it needs to get the information of the authenticated user. The AuditorAware interface has one type parameter (T) which describes the type of the entity’s field that contains the auditing information.
Because we have to create a class that returns the username of the authenticated user, we have to follow these steps:
- Create UsernameAuditorAware class and implement the AuditorAware interface. Because we want to store the username of the authenticated user (String), we must set the value of the type parameter to String.
- Implement the getCurrentAuditor() method by following these steps:
- Get an Authentication object from the SecurityContext.
- Return null if the authentication is not found or the found authentication is not authenticated.
- Return the username of the authenticated user.
The source code of the UsernameAuditorAware class looks as follows:
1import org.springframework.data.domain.AuditorAware;
2import org.springframework.security.core.Authentication;
3import org.springframework.security.core.context.SecurityContextHolder;
4import org.springframework.security.core.userdetails.User;
5
6public class UsernameAuditorAware implements AuditorAware<String> {
7
8 @Override
9 public String getCurrentAuditor() {
10 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
11
12 if (authentication == null || !authentication.isAuthenticated()) {
13 return null;
14 }
15
16 return ((User) authentication.getPrincipal()).getUsername();
17 }
18}
Additional Reading:
Let’s move on and find out how we can configure the application context of our example application.
Configuring the Application Context
We can configure the application context of our application by making the following changes to the configuration class that configures the persistence layer of our application:
- Create an auditorProvider() method that returns an AuditorAware<String> object.
- Implement the method by creating a new UsernameAuditorAware object.
- Annotate the method with the @Bean annotation.
- Enable the auditing support of Spring Data JPA by annotating the configuration class with the @EnableJpaAuditing annotation.
The relevant part of the PersistenceContext class looks as follows:
1import org.springframework.context.annotation.Bean;
2import org.springframework.context.annotation.Configuration;
3import org.springframework.data.auditing.DateTimeProvider;
4import org.springframework.data.domain.AuditorAware;
5import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
7import org.springframework.transaction.annotation.EnableTransactionManagement;
8
9@Configuration
10@EnableJpaAuditing(dateTimeProviderRef = "dateTimeProvider")
11@EnableJpaRepositories(basePackages = {
12 "net.petrikainulainen.springdata.jpa.todo"
13})
14@EnableTransactionManagement
15class PersistenceContext {
16
17 @Bean
18 AuditorAware<String> auditorProvider() {
19 return new UsernameAuditorAware();
20 }
21
22 @Bean
23 DateTimeProvider dateTimeProvider(DateTimeService dateTimeService) {
24 return new AuditingDateTimeProvider(dateTimeService);
25 }
26}
Because we declared only one AuditorAware bean, the auditing infrastructure finds it automatically and uses it when it has to set the information of the authenticated user to the field(s) of the saved or updated entity object. If we declare multiple AuditorAware beans, we can configure the used bean by setting the value of the auditorAwareRef attribute of the @EnableJpaAuditing annotation.
If you want use XML configuration, you can enable the auditing support by using the auditing element. Take look at the example application of this blog post that has a working XML configuration file.
Additional Reading:
Let’s move on and make the required changes to our entity class.
Modifying Our Entity Class
We need to make the following changes to our entity class (Todo):
- We need to ensure that the value of the createdByUser field is set when our entity is persisted for the first time.
- We need to ensure that the value of the modifiedByUser field is set when our entity is persisted for the first time and updated when the information of our entity is updated.
We can make these changes by following these steps:
- Add a createdByUser field into the entity class, set its type to String, and follow these steps:
- Annotate the field with the @Column annotation. Configure the name of the database column (created_by_user) and ensure that the value of this column cannot be null.
- Annotate the field with the @CreatedBy annotation. This identifies the field that contains the information of the user who created the entity.
- Add a modifiedByUser field into the entity class, set its type to String, and follow these steps
- Annotate the field with the @Column annotation. Configure the name of the database column (modified_by_user) and ensure that the value of this column cannot be null.
- Annotate the field with the @LastModifiedBy annotation. This identifies the field that contains the information of the user who made the last changes to the entity.
The relevant part of the Todo class looks as follows:
1import org.hibernate.annotations.Type;
2import org.springframework.data.annotation.CreatedBy;
3import org.springframework.data.annotation.CreatedDate;
4import org.springframework.data.annotation.LastModifiedBy;
5import org.springframework.data.annotation.LastModifiedDate;
6import org.springframework.data.jpa.domain.support.AuditingEntityListener;
7
8import javax.persistence.Column;
9import javax.persistence.Entity;
10import javax.persistence.EntityListeners;
11import javax.persistence.GeneratedValue;
12import javax.persistence.GenerationType;
13import javax.persistence.Id;
14import javax.persistence.Table;
15import javax.persistence.Version;
16import java.time.ZonedDateTime;
17
18@Entity
19@EntityListeners(AuditingEntityListener.class)
20@Table(name = "todos")
21final class Todo {
22
23 @Id
24 @GeneratedValue(strategy = GenerationType.AUTO)
25 private Long id;
26
27 @Column(name = "created_by_user", nullable = false)
28 @CreatedBy
29 private String createdByUser;
30
31 @Column(name = "creation_time", nullable = false)
32 @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime")
33 @CreatedDate
34 private ZonedDateTime creationTime;
35
36 @Column(name = "description", length = 500)
37 private String description;
38
39 @Column(name = "modified_by_user", nullable = false)
40 @LastModifiedBy
41 private String modifiedByUser;
42
43 @Column(name = "modification_time")
44 @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime")
45 @LastModifiedDate
46 private ZonedDateTime modificationTime;
47
48 @Column(name = "title", nullable = false, length = 100)
49 private String title;
50
51 @Version
52 private long version;
53}
Typically it is a good idea to add the audit fields into an abstract base class. The reason why I didn’t do it here is that our example application has only one entity, and I wanted to keep things as simple as possible.
If we would move this information to the abstract base class, its source code would look as follows:
1import org.hibernate.annotations.Type;
2import org.springframework.data.annotation.CreatedBy;
3import org.springframework.data.annotation.CreatedDate;
4import org.springframework.data.annotation.LastModifiedBy;
5import org.springframework.data.annotation.LastModifiedDate;
6import org.springframework.data.jpa.domain.support.AuditingEntityListener;
7
8import javax.persistence.Column;
9import javax.persistence.MappedSuperClass
10
11@EntityListeners(AuditingEntityListener.class)
12@MappedSuperClass
13public abstract class BaseEntity {
14
15 @Column(name = "created_by_user", nullable = false)
16 @CreatedBy
17 private String createdByUser;
18
19 @Column(name = "creation_time", nullable = false)
20 @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime")
21 @CreatedDate
22 private ZonedDateTime creationTime;
23
24 @Column(name = "modified_by_user", nullable = false)
25 @LastModifiedBy
26 private String modifiedByUser;
27
28 @Column(name = "modification_time")
29 @Type(type = "org.jadira.usertype.dateandtime.threeten.PersistentZonedDateTime")
30 @LastModifiedDate
31 private ZonedDateTime modificationTime;
32}
If you don’t want to use annotations, your entities must either implement the Auditable interface or extend the AbstractAuditable class. The Auditable interface declares getter and setter methods for all audit fields. The AbstractAuditable class provides implementations for these methods, but its drawback is that it creates a coupling between your entities and Spring Data.
Additional Reading:
Let’s find out why we should use the auditing support of Spring Data JPA instead of the callback methods specified in the Java Persistence API.
Why Should We Use the Auditing Infrastructure of Spring Data JPA?
If we want to save the information of the user who created and updated our entity, we don’t have to use Spring Data JPA. We can set the values of these fields (createdByUser and modifiedByUser) by creating callback methods that are attached to the entity lifecycle events.
The source code of an abstract base class, which uses this method, looks as follows:
1import org.springframework.data.annotation.CreatedBy;
2import org.springframework.data.annotation.LastModifiedBy;
3
4import org.springframework.security.core.Authentication;
5import org.springframework.security.core.context.SecurityContextHolder;
6import org.springframework.security.core.userdetails.User;
7
8import javax.persistence.Column;
9import javax.persistence.MappedSuperClass
10import javax.persistence.PrePersist;
11import javax.persistence.PreUpdate;
12
13@MappedSuperClass
14public abstract class BaseEntity {
15
16 @Column(name = "created_by_user", nullable = false)
17 @CreatedBy
18 private String createdByUser;
19
20 @Column(name = "modified_by_user", nullable = false)
21 @LastModifiedBy
22 private String modifiedByUser;
23
24 @PrePersist
25 public void prePersist() {
26 String createdByUser = getUsernameOfAuthenticatedUser();
27 this.createdByUser = createdByUser;
28 this.modifiedByUser = createdByUser;
29 }
30
31 @PreUpdate
32 public void preUpdate() {
33 String modifiedByUser = getUsernameOfAuthenticatedUser();
34 this.modifiedByUser = modifiedByUser;
35 }
36
37 private String getUsernameOfAuthenticatedUser() {
38 Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
39
40 if (authentication == null || !authentication.isAuthenticated()) {
41 return null;
42 }
43
44 return ((User) authentication.getPrincipal()).getUsername();
45 }
46}
Additional Reading:
Even though this method is a bit simpler and more straightforward than using the auditing infrastructure of Spring Data JPA, there are two reasons why we should consider using a more complex solution:
First, using callback methods creates a coupling between our base class (or entity classes) and Spring Security, and I want to avoid it.
Second, if we need to set the values of the creation and modification time fields AND we decided to use the auditing infrastructure of Spring Data JPA for that purpose, we should use it for setting the field values of the createdByUser and modifiedByUser fields because it makes no sense to set the audit information by using two different mechanisms.
Let’s summarize what we learned from this blog post.
Summary
This blog post has taught us three things:
- The AuditorAware<T> interface declares the method that provides the information of the authenticated user to the auditing infrastructure of Spring Data JPA.
- We can identify the audit fields by using annotations, set the values by implementing the Auditable interface, or extend the AbstractAuditable class.
- It is simpler to set the values of the audit fields by using callback methods that are attached to the entity lifecycle events. The downside of this method is that it creates a coupling between our abstract base class (or entity classes) and Spring Security.
The next part of this tutorial describes how we can add custom methods into a single repository.
P.S. You can get the example application of this blog post from Github.