Contents
- Enable JPA Auditing
- AuditorAware — Current User
- Auditable Base Entity
- @EntityListeners & Custom Callbacks
- Auditing Annotations Reference
- Hibernate Envers — Full Revision History
- Testing Auditing
Enable the feature by adding the annotation shown below to your main application class or to a dedicated @Configuration class.
@SpringBootApplication
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Alternatively, place @EnableJpaAuditing on a @Configuration class. The auditorAwareRef attribute names the bean that returns the current user — needed for @CreatedBy and @LastModifiedBy.
The class below shows the implementation. Key points are highlighted in the inline comments.
@Component("auditorProvider")
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Pull the authenticated username from Spring Security
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
For systems without Spring Security you can supply a different source — a request-scoped bean holding the tenant user, an MDC value, or a system account name for background jobs.
// Simpler AuditorAware for non-security projects or batch jobs
@Component("auditorProvider")
public class StaticAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Could read from MDC, thread-local, or request context
String user = RequestContextHolder.getRequestAttributes() != null
? ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("X-User-Id")
: "system";
return Optional.ofNullable(user).filter(s -> !s.isEmpty());
}
}
Create a @MappedSuperclass that all audited entities extend. Spring Data will populate the annotated fields on every persist and merge.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@CreatedBy
@Column(name = "created_by", nullable = false, updatable = false, length = 100)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by", nullable = false, length = 100)
private String updatedBy;
// Getters (no setters — Spring Data sets these internally)
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public String getCreatedBy() { return createdBy; }
public String getUpdatedBy() { return updatedBy; }
}
@Entity
@Table(name = "orders")
public class Order extends AuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String customerId;
private BigDecimal total;
@Enumerated(EnumType.STRING)
private OrderStatus status;
// business fields — audit fields come from AuditableEntity
}
-- Corresponding migration (Flyway V3 or Liquibase changeSet)
ALTER TABLE orders
ADD COLUMN created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ADD COLUMN created_by VARCHAR(100) NOT NULL DEFAULT 'system',
ADD COLUMN updated_by VARCHAR(100) NOT NULL DEFAULT 'system';
Beyond the built-in AuditingEntityListener, you can write custom listeners to run business logic on JPA lifecycle events.
public class OrderAuditListener {
private static final Logger log = LoggerFactory.getLogger(OrderAuditListener.class);
@PrePersist
public void onPrePersist(Order order) {
log.info("Creating order for customer: {}", order.getCustomerId());
}
@PreUpdate
public void onPreUpdate(Order order) {
log.info("Updating order {} — new status: {}", order.getId(), order.getStatus());
}
@PostRemove
public void onPostRemove(Order order) {
log.warn("Order {} was deleted", order.getId());
}
}
@Entity
@Table(name = "orders")
@EntityListeners({ AuditingEntityListener.class, OrderAuditListener.class })
public class Order extends AuditableEntity {
// ...
}
Entity listeners are plain POJOs — they are not Spring beans by default. To inject Spring beans into a listener, configure spring.jpa.properties.hibernate.ejb.interceptor or use a BeanFactoryAware lookup. Spring Data's own AuditingEntityListener handles this internally.
| Annotation | Filled on | Type | Description |
| @CreatedDate | First save | Instant, LocalDateTime, Date | Timestamp when the entity was created |
| @LastModifiedDate | Every save | Same as above | Timestamp of last update |
| @CreatedBy | First save | String, custom type | User who created — from AuditorAware |
| @LastModifiedBy | Every save | Same as above | User who last updated — from AuditorAware |
| @Version | Every save | Long, Integer | Optimistic locking version counter (not auditing, but related) |
When you need a full audit log (every historical value, not just the current created/updated timestamps), use Hibernate Envers. It creates a _AUD table per entity containing every version ever saved.
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-envers</artifactId>
</dependency>
@Entity
@Audited // enables Envers for this entity
@Table(name = "orders")
public class Order extends AuditableEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotAudited // exclude this field from revision history
private String internalNote;
private BigDecimal total;
@Enumerated(EnumType.STRING)
private OrderStatus status;
}
// Querying revision history
@Repository
public class OrderAuditRepository {
@PersistenceContext
private EntityManager em;
public List<Order> getOrderHistory(Long orderId) {
AuditReader reader = AuditReaderFactory.get(em);
return reader.createQuery()
.forRevisionsOfEntity(Order.class, true, true)
.add(AuditEntity.id().eq(orderId))
.addOrder(AuditEntity.revisionNumber().asc())
.getResultList();
}
public Order getOrderAtRevision(Long orderId, Number revision) {
AuditReader reader = AuditReaderFactory.get(em);
return reader.find(Order.class, orderId, revision);
}
public List<Number> getRevisionNumbers(Long orderId) {
AuditReader reader = AuditReaderFactory.get(em);
return reader.getRevisions(Order.class, orderId);
}
}
The class below shows the implementation. Key points are highlighted in the inline comments.
@DataJpaTest
@Import(SecurityAuditorAware.class)
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class OrderAuditingTest {
@Autowired
private OrderRepository orderRepository;
@Test
void createdDateAndCreatedByAreSetOnFirstSave() {
// Simulate an authenticated user
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("alice", null, List.of()));
Order order = new Order();
order.setCustomerId("CUST-1");
order.setTotal(new BigDecimal("99.99"));
order.setStatus(OrderStatus.PENDING);
Order saved = orderRepository.save(order);
assertThat(saved.getCreatedAt()).isNotNull();
assertThat(saved.getUpdatedAt()).isNotNull();
assertThat(saved.getCreatedBy()).isEqualTo("alice");
assertThat(saved.getUpdatedBy()).isEqualTo("alice");
SecurityContextHolder.clearContext();
}
@Test
void updatedByChangesOnUpdate() {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("alice", null, List.of()));
Order saved = orderRepository.save(newOrder());
SecurityContextHolder.clearContext();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("bob", null, List.of()));
saved.setStatus(OrderStatus.CONFIRMED);
Order updated = orderRepository.save(saved);
assertThat(updated.getCreatedBy()).isEqualTo("alice"); // unchanged
assertThat(updated.getUpdatedBy()).isEqualTo("bob"); // updated
SecurityContextHolder.clearContext();
}
}