Auditing with Hibernate Envers is a small thing to implement but is the easiest way to audit persistent data in a Spring Boot application. However, Envers is opinionated and may not meet data auditing requirements for your organization, such as audit table schema design or content.
Spring Boot applications that require more flexible auditing capabilities can implement auditing using Spring Data JPA.
Auditing approach
This approach implements callbacks for JPA lifecycle events. The sample implementation abstracts common code and common audit data to suit applications that work with many entities:
- A listener class for each JPA entity encapsulates callback methods for each CUD operation on the entity.
- Each listener class extends a common abstract listener class.
- Each entity registers its listener class via a Spring Data JPA annotation.
The Abstract Listener Class
Code common for entity listener callbacks is provided by an abstract listener class.
abstract class AbstractEntityListener<E> { void doCreate(E entity) { postProcess(entity, “CREATE”); } void doUpdate(E entity) { postProcess(entity, ”UPDATE”); } void doDelete(E entity) { postProcess(entity, “DELETE”); } private void postProcess(E entity, String action) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronizationAdapter() { @Override public void afterCompletion(int status) { if (status == STATUS_COMMITTED) { persist(entity, action); } } }); } private void persist(E entity, String action) { EntityManagerFactory entityManagerFactory = AuditUtil.getBean(EntityManagerFactory.class); EntityManager entityManager = entityManagerFactory.createEntityManager(); AuditBase auditEntity = mapAuditEntity(entity); auditEntity.setAuditAction(action); auditEntity.setAuditDate(Instant.now()); entityManager.getTransaction().begin(); entityManager.persist(auditEntity); entityManager.flush(); entityManager.getTransaction().commit(); entityManager.close(); } abstract AuditBase mapAuditEntity(E entity); }
NOTES:
- The doCreate, doUpdate, and doDelete provide default implementations for lifecycle callbacks on each of the CUD operations on the entity.
- The postProcess method uses Spring transaction synchronization for fine-grained control on when entity auditing occurs. In this example, the audit data is persisted only after the change to the corresponding entity is actually committed.
- The persist method does the actual work of persisting the audit data by creating an EntityManager object and using it to do the work.
- Each audit entity extends AuditBase, which provides common audit fields.
- Entity listeners are instantiated by JPA and therefore Spring dependency injection is not supported within an entity listener class. The AuditUtil class provides the mechanism for obtaining Spring beans within entity listeners.
The AuditUtil Class
This class implements ApplicationContextAware and provides method getBean(), which returns a Spring bean of the requested class.
@Component public class AuditUtil implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { setContext(applicationContext); } public static<T> T getBean(Class<T> beanClass) { return context.getBean(beanClass); } private static void setContext(ApplicationContext context) { AuditUtil.context = context; } }
The Entity Listener Class
The listener class for each entity provides the actual JPA callbacks.
public class MyEntityListener extends AbstractEntityListener<MyEntity> { @PostPersist public void onCreate(MyEntity entity) { // Custom updates on the modified entity, if any. doCreate(entity); } @PostUpdate public void onUpdate(MyEntity entity) { // Custom updates on the modified entity, if any. doUpdate(entity); } @PostRemove public void onDelete(MyEntity entity) { // Custom updates on the modified entity, if any. doDelete(entity); } @Override public MyEntityAudit mapAuditEntity(MyEntity entity) { // Populate the audit entity from data in the modified entity. } }
NOTES:
- The annotation on each CUD method registers the lifecycle callback with JPA.
- The mapAuditEntity method provides the code that copies data from the entity to its corresponding audit entity. Mappers such as mapstuct or modelMapper are good choices when the audit data closely matches the entity data.
The Entity Class
Each entity class is written using normal JPA annotations but also registers its entity listener class with JPA.
@Entity @Table(name = “MY_ENTITY”) @EntityListeners(MyEntityListener.class) Public class MyEntity { … }
The Entity Audit Class
An entity audit class is written using normal JPA annotations and the audit data can be customized in any way that satisfies your organization’s data auditing requirements. In this sample implementation, each entity audit class extends AuditBase, which provides common audit data.
@Entity @Table(name = “MY_ENTITY_AUDIT”) public class MyEntityAudit extends AuditBase { … }
The AuditBase Class
AuditBase provides common audit data.
@MappedSuperclass public abstract class AuditBase { @Column(name = “AUDIT_ACTION”) private String auditAction; @Column(name = “AUDITED_AT”) private Instant auditedAt; @Column(name = “CREATED_BY”) private String createdBy; @Column(name = “CREATED_AT”) private Instant createdAt; @Column(name = “UPDATED_BY”) private String updatedBy; @Column(name = “UPDATED_AT”) private Instant updatedAt; }
References: