Spring Boot
Auditing
Spring Data JPA auditing automatically populates created and modified timestamps and user fields on entities. @EnableJpaAuditing activates the infrastructure; @CreatedDate, @LastModifiedDate, @CreatedBy, and @LastModifiedBy on entity fields are populated before every INSERT and UPDATE. This entry covers setup, a shared base entity, custom AuditorAware, event-based auditing with @EntityListeners, and full audit history with Hibernate Envers.
Setup and @EnableJpaAuditing
Add @EnableJpaAuditing to a @Configuration class or the main application class to activate Spring Data's auditing infrastructure. Once enabled, @EntityListeners(AuditingEntityListener.class) on an entity class — or on a @MappedSuperclass shared by all entities — instructs the listener to populate the annotated fields before every persist and merge.
Java
// ── Enable auditing ───────────────────────────────────────────────────
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
// auditorAwareRef wires the AuditorAware bean that supplies
// the current user's identifier for @CreatedBy / @LastModifiedBy
}
// ── Alternatively on the main class ───────────────────────────────────
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// ── AuditorAware — supplies the current principal ─────────────────────
@Component("auditorProvider")
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.filter(auth -> !"anonymousUser".equals(auth.getPrincipal()))
.map(Authentication::getName);
}
}
// ── For non-Spring-Security applications ──────────────────────────────
@Component("auditorProvider")
public class RequestContextAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Could read from a ThreadLocal, MDC, or request attribute
return Optional.ofNullable(
RequestContextHolder.getRequestAttributes())
.map(attrs -> (ServletRequestAttributes) attrs)
.map(ServletRequestAttributes::getRequest)
.map(req -> (String) req.getAttribute("authenticatedUser"));
}
}Auditable Base Entity
Define a @MappedSuperclass that carries all four audit fields and annotate it with @EntityListeners(AuditingEntityListener.class). Every entity that extends it inherits auditing without any additional configuration. This is the most maintainable pattern for applications with many entities.
Java
// ── Shared auditable base ─────────────────────────────────────────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@SuperBuilder
@NoArgsConstructor
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by", length = 100)
private String updatedBy;
}
// ── Entity extending the base ─────────────────────────────────────────
@Entity
@Table(name = "products")
@Getter @Setter
@NoArgsConstructor
@SuperBuilder
public class Product extends AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status;
}
// ── Result — what Hibernate sets automatically ─────────────────────────
// On first save():
// created_at = 2024-03-15T10:30:00
// updated_at = 2024-03-15T10:30:00
// created_by = "alice"
// updated_by = "alice"
//
// On subsequent save():
// updated_at = 2024-03-15T11:45:00 ← updated
// updated_by = "bob" ← updated
// created_at unchanged ← updatable = false
// created_by unchanged ← updatable = falseAuditing with UUID and Long Auditor Types
AuditorAware<T> is generic — T can be String (username), Long (user ID), or UUID (user UUID). Match T to the type of @CreatedBy and @LastModifiedBy fields on the entity. Using a Long or UUID user ID avoids storing mutable usernames and is better for foreign key references.
Java
// ── Long user ID auditing ─────────────────────────────────────────────
@Component("auditorProvider")
public class LongAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
return Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.filter(p -> p instanceof UserPrincipal)
.map(p -> ((UserPrincipal) p).getId());
}
}
// ── Base entity with Long auditor ─────────────────────────────────────
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class LongAuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@CreatedBy
@Column(name = "created_by_id", updatable = false)
private Long createdById;
@LastModifiedBy
@Column(name = "updated_by_id")
private Long updatedById;
}
// ── Enable with explicit type ──────────────────────────────────────────
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaConfig {
@Bean
public AuditorAware<Long> auditorProvider() {
return new LongAuditorAware();
}
}
// ── UUID auditor ───────────────────────────────────────────────────────
@Component("auditorProvider")
public class UuidAuditorAware implements AuditorAware<UUID> {
@Override
public Optional<UUID> getCurrentAuditor() {
return Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getPrincipal)
.filter(p -> p instanceof UserPrincipal)
.map(p -> ((UserPrincipal) p).getUuid());
}
}Lifecycle Callbacks with @PrePersist and @PreUpdate
JPA lifecycle callbacks are an alternative to Spring Data's AuditingEntityListener when Spring context is not available — in tests, batch jobs, or non-Spring-managed environments. @PrePersist runs before INSERT; @PreUpdate runs before UPDATE. They live on the entity itself or on a separate EntityListener class.
Java
// ── Lifecycle callbacks directly on the entity ───────────────────────
@Entity
@Table(name = "orders")
@Getter @Setter @NoArgsConstructor
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal total;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now(ZoneOffset.UTC);
}
}
// ── External EntityListener class ─────────────────────────────────────
public class TimestampEntityListener {
@PrePersist
public void prePersist(Object entity) {
if (entity instanceof Timestamped t) {
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
t.setCreatedAt(now);
t.setUpdatedAt(now);
}
}
@PreUpdate
public void preUpdate(Object entity) {
if (entity instanceof Timestamped t) {
t.setUpdatedAt(LocalDateTime.now(ZoneOffset.UTC));
}
}
}
// ── Interface for the listener to target ──────────────────────────────
public interface Timestamped {
void setCreatedAt(LocalDateTime createdAt);
void setUpdatedAt(LocalDateTime updatedAt);
}
// ── Register on a @MappedSuperclass ───────────────────────────────────
@MappedSuperclass
@EntityListeners(TimestampEntityListener.class)
public abstract class TimestampedEntity implements Timestamped {
@Column(name = "created_at", updatable = false)
@Setter private LocalDateTime createdAt;
@Column(name = "updated_at")
@Setter private LocalDateTime updatedAt;
}Full Audit History with Hibernate Envers
Spring Data auditing records the current state only. Hibernate Envers records every historical revision — who changed what and when — in audit tables. Add the dependency, annotate entities with @Audited, and query history through AuditReader. Envers creates a shadow table for each audited entity with a _AUD suffix.
XML
<!-- pom.xml -->
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
// ── Audited entity ────────────────────────────────────────────────────
@Entity
@Table(name = "products")
@Audited // Envers audits every change
@Getter @Setter @NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status;
}
// ── Envers creates these tables automatically ──────────────────────────
// REVINFO — revision metadata (id, timestamp)
// PRODUCTS_AUD — one row per revision per product
// id, REV, REVTYPE (0=ADD, 1=MOD, 2=DEL), name, price, status
// ── Custom revision entity — add username to each revision ───────────
@Entity
@RevisionEntity(UserRevisionListener.class)
@Table(name = "revinfo")
@Getter @Setter
public class UserRevisionEntity extends DefaultRevisionEntity {
@Column(name = "username", length = 100)
private String username;
}
@Component
public class UserRevisionListener implements RevisionListener {
@Override
public void newRevision(Object revisionEntity) {
UserRevisionEntity rev = (UserRevisionEntity) revisionEntity;
Optional.ofNullable(
SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getName)
.ifPresent(rev::setUsername);
}
}
// ── Query audit history ────────────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class ProductAuditService {
@PersistenceContext
private EntityManager entityManager;
// All revisions of a product
public List<ProductAuditEntry> findHistory(Long productId) {
AuditReader reader = AuditReaderFactory.get(entityManager);
return reader
.createQuery()
.forRevisionsOfEntity(Product.class, false, true)
.add(AuditEntity.id().eq(productId))
.addOrder(AuditEntity.revisionNumber().desc())
.getResultList()
.stream()
.map(row -> {
Object[] tuple = (Object[]) row;
Product product = (Product) tuple[0];
UserRevisionEntity revision = (UserRevisionEntity) tuple[1];
RevisionType type = (RevisionType) tuple[2];
return new ProductAuditEntry(
product, revision.getRevisionDate(),
revision.getUsername(), type.name());
})
.toList();
}
// State of a product at a specific revision
public Product findAtRevision(Long productId, int revision) {
AuditReader reader = AuditReaderFactory.get(entityManager);
return reader.find(Product.class, productId, revision);
}
}