Spring Data provides sophisticated support to transparently keep track of who created or changed an entity and the point in time this happened.

Introduction

Spring Data provides an abstraction layer to easily create a data access layer to a datastore. Check out the CRUD Operations with Spring Boot for an introduction to Spring Data. In this post we will learn how to apply Entity Auditing using Spring Data. We will be using @CreatedBy, @LastModifiedBy to capture the user who created or modified the entity as well as @CreatedDate and @LastModifiedDate to capture the point in time this happened.

Prerequisites

Project Structure

At the end of this guide our folder structure will look similar to the following:

.
|__src/
|  |__main/
|  |  |__java/
|  |  |  |__com/
|  |  |  |  |__juliuskrah/
|  |  |  |  |  |__audit/
|  |  |  |  |  |  |__Application.java
|  |  |  |  |  |  |__Customer.java
|  |  |  |  |  |  |__CustomerHandler.java
|  |  |  |  |  |  |__CustomerRepository.java
|  |  |  |  |  |  |__SpringSecurityAuditorAware .java
|  |  |__resources/
|  |  |  |__db/
|  |  |  |  |__changelog/
|  |  |  |  |  |__db.changelog-master.yaml
|  |  |  |__application.yaml
|  |__test/
|  |  |__java/
|  |  |  |__com/
|  |  |  |  |__juliuskrah/
|  |  |  |  |  |__audit/
|  |  |  |  |  |  |__ApplicationTests.java
|__pom.xml

What You Need to Get Started

To help readers be able to follow along and do hands-on, I have provided an initial code that you can download as zip/tar.gz. Go ahead download and extract, and import into your favorite IDE as a maven project. Run and confirm everything works.

Create the Required Auditing Classes

The first order of business is to create an Entity class and add the Spring Data auditing metadata:

file: src/main/java/com/juliuskrah/audit/Customer.java

@Entity
@EntityListeners(AuditingEntityListener.class)
public class Customer implements Serializable {

  @Id
  @GeneratedValue(generator = "uuid2")
  @GenericGenerator(name = "uuid2", strategy = "uuid2")
  private String id;
  private String name;
  private String email;
  private String order;
  @CreatedBy                      // Audit metadata
  @Column(updatable = false)
  private String createdBy;
  @CreatedDate                    // Audit metadata
  @Column(updatable = false)
  private LocalDateTime createdDate;
  @LastModifiedBy                 // Audit metadata
  private String lastModifiedBy;
  @LastModifiedDate               // Audit metadata
  private LocalDateTime lastModifiedDate;

  // Getters and Setters omitted for brevity
}

AuditingEntityListener is a JPA entity listener to capture auditing information on persiting and updating entities.

Activate the Spring Data auditing feature by annotating the main configuration class with @EnableJpaAuditing:

file: src/main/java/com/juliuskrah/audit/Application.java

@EnableJpaAuditing
@SpringBootApplication
public class Application {
    //
}

Let’s add some CrudRepository:

file: src/main/java/com/juliuskrah/audit/CustomerRepository.java

public interface CustomerRepository extends CrudRepository<Customer, String> {}

In our case, we are using @CreatedBy and @LastModifiedBy, the auditing infrastructure somehow needs to become aware of the current principal. To do so, we implement the AuditorAware<T> SPI interface to tell the infrastructure who the current user or system interacting with the application is. The generic type T defines of what type the properties annotated with @CreatedBy or @LastModifiedBy have to be:

file: src/main/java/com/juliuskrah/audit/SpringSecurityAuditorAware.java

@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {

  @Override
  public Optional<String> getCurrentAuditor() {
    return ReactiveSecurityContextHolder.getContext()
      .map(SecurityContext::getAuthentication)
      .filter(Authentication::isAuthenticated)
      .map(Authentication::getName)
      .switchIfEmpty(Mono.just("julius"))
      .blockOptional();
  }
}

We need to register the AuditorAware implementation with the Auditing infrastructure:

file: src/main/java/com/juliuskrah/audit/Application.java

@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@SpringBootApplication
public class Application {
    //
}

Take note of the auditorAwareRef attribute with value springSecurityAuditorAware. The springSecurityAuditorAware is the bean name of the AuditorAware implementation.

Test the @CreatedDate and @LastModifiedDate

Now we need to verify the auditing infrastructure works as expected. We will write two test cases to verify this:

file: src/test/java/com/juliuskrah/audit/ApplicationTests.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
  @Autowired
  private CustomerRepository repository;

  @Test
  public void customerWhenCreateThenCreatedDateIsNotNull() {
    Customer customer = new Customer();
    customer.setName("James Gunn");
    customer.setEmail("jamesgunn@gmail.com");

    repository.save(customer);

    assertThat(customer.getId()).isNotNull();
    assertThat(customer.getCreatedDate()).isNotNull();   // CreatedBy added by Spring Audit
    assertThat(customer.getCreatedDate()).isBefore(now());
  }
	
  @Test
  public void customerWhenUpdateThenLastModifiedDateIsAfterCreatedDate() {
    Customer customer = new Customer();
    customer.setName("Tyler Perry");
    customer.setEmail("tylerperry@gmail.com");

    repository.save(customer);
    Optional<Customer> c = repository.findById(customer.getId());
		
    assertThat(c.isPresent()).isTrue();
		
    customer = c.get();
    customer.setOrder("Samsung Galaxy");
		
    customer = repository.save(customer);
		
    assertThat(customer.getLastModifiedDate()).isAfter(customer.getCreatedDate()); // Last Modified By added by Spring Audit
  }
}

Add Some Routes

This step is added to test the Principal injected for @CreatedBy and @LastModifiedBy. We will first create a HandlerFuntion to process our requests:

file: src/main/java/com/juliuskrah/audit/CustomerHandler.java

@Component
public class CustomerHandler {
  private final CustomerRepository repository;

  public CustomerHandler(CustomerRepository repository) {
    this.repository = repository;
  }

  public Mono<ServerResponse> retrieveCustomers(ServerRequest request) {
    Iterable<Customer> customers = repository.findAll();
    return ServerResponse.ok().body(Mono.just(customers), 
      new ParameterizedTypeReference<Iterable<Customer>>() {});
  }

  public Mono<ServerResponse> retrieveCustomer(ServerRequest request) {
    String id = request.pathVariable("id");
    Optional<Customer> customer = repository.findById(id);
    if (customer.isPresent())
      return ServerResponse.ok().body(Mono.just(customer.get()), Customer.class);
    return ServerResponse.notFound().build();
  }

  public Mono<ServerResponse> createCustomer(ServerRequest request) {
    Mono<Customer> customer = request.bodyToMono(Customer.class);

    return customer.flatMap(c -> {
      repository.save(c);
      URI uri = request.uriBuilder().path("/{id}").build(c.getId());
      return ServerResponse.created(uri).build();
    });
  }

  public Mono<ServerResponse> updateCustomer(ServerRequest request) {
    Mono<Customer> customer = request.bodyToMono(Customer.class);

    return customer.flatMap(c -> {
      String id = request.pathVariable("id");
      Optional<Customer> custom = repository.findById(id);
      if (custom.isPresent()) {
        String order = c.getOrder();
        c = custom.get();
        c.setOrder(order);
        repository.save(c);
      } else
        return ServerResponse.notFound().build();
      return ServerResponse.noContent().build();
    });
  }

  public Mono<ServerResponse> deleteCustomer(ServerRequest request) {
    String id = request.pathVariable("id");
    repository.deleteById(id);

    return ServerResponse.noContent().build();
  }
}

Add a RouterFunction to the main configuration class:

file: src/main/java/com/juliuskrah/audit/Application.java

@EnableJpaAuditing
@SpringBootApplication
public class Application {
  //
    
  @Bean
  public RouterFunction<ServerResponse> customerRouter(CustomerHandler customerHandler) {
    return nest(path("/customers"),
      route(GET("/").and(accept(APPLICATION_JSON)), customerHandler::retrieveCustomers)
      .andRoute(GET("/{id}").and(accept(APPLICATION_JSON)), customerHandler::retrieveCustomer)
      .andRoute(POST("/").and(contentType(APPLICATION_JSON)), customerHandler::createCustomer)
      .andRoute(PUT("/{id}").and(contentType(APPLICATION_JSON)), customerHandler::updateCustomer)
      .andRoute(DELETE("/{id}"), customerHandler::deleteCustomer));
  }
}

Create a default username and password:

file: src/main/resources/application.yaml

spring:
  security:
    user:
      name: julius
      password: secret

Test the @CreatedBy and @LastModifiedBy

We need to verify the auditing infrastructure works as expected. We will write two test cases to verify this:

file: src/test/java/com/juliuskrah/audit/ApplicationTests.java

@SpringBootTest
public class ApplicationTests {
  //

  @Autowired
  private ApplicationContext context;
  private WebTestClient rest;

  @Before
  public void setup() {
    this.rest = WebTestClient
      .bindToApplicationContext(this.context)
      .apply(springSecurity())
      .configureClient()
      .filter(basicAuthentication("julius", "secret"))
      .build();
  }

  @Test
  public void customerWhenCreateThenCreatedByIsNotNull() {
    Customer customer = new Customer();
    customer.setName("Jack Sparrow");
    customer.setEmail("jacksparrow@strangertides.com");
		
    this.rest
      .mutateWith(csrf())
      .post()
      .uri("/customers")
      .body(Mono.just(customer), Customer.class)
      .exchange()
      .expectStatus().isCreated();
		
    this.rest
      .get()
      .uri("/customers")
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$[0].createdBy").exists();
  }
	
  @Test
  public void customerWhenUpdateThenLastModifiedDateIsNotNull() {
    Customer customer = new Customer();
    customer.setName("Elizabeth Swarn");
    customer.setEmail("lizzyswarn@strangertides.com");
		
    repository.save(customer);
    customer.setOrder("food stuff");
		
    this.rest
      .mutateWith(csrf())
      .put()
      .uri("/customers/{id}", customer.getId())
      .body(Mono.just(customer), Customer.class)
      .exchange()
      .expectStatus().isNoContent();
		
    this.rest
      .get()
      .uri("/customers/{id}", customer.getId())
      .exchange()
      .expectStatus().isOk()
      .expectBody()
      .jsonPath("$.lastModifiedBy").exists();
  }
}

That’s all folks

Conclusion

In this post we looked at Spring Data’s built-in auditing infrastructure with minimal configuration leveraging Spring Webflux and Reactive Spring Security.

You can find the source to this guide in the github repository. Until the next post, keep doing cool things :+1:.