Interceptors, as the name suggests, provide callbacks to certain events that occur inside Hibernate. It helps in implementing AOP style cross-cutting concerns and the extension of Hibernate functionality.
1. Creating an Interceptor
To create a new Interceptor in Hibernate, we need to implement the org.hibernate.Interceptor
interface. This interface provides methods to inspect and/or manipulate properties of a persistent object before it is saved, updated, deleted or loaded.
Before Hibernate 6.0, extending the EmptyInterceptor
was a preferred way to override only the necessary methods because to implement Interceptor, we have to implement all 14 methods in the interface. This was obviously not suitable until we had a very strong reason to do it.
Since Hibernate 6.0, EmptyInterceptor
has been deprecated. And the methods inside Interceptor
interface have been made default methods, so we only need to override only the necessary method now.
public class AuditInterceptor implements Interceptor {
}
2. Overriding Interceptor Methods
Interceptor interface provides the following important methods for intercepting specific events:
- afterTransactionBegin(): Called when a Hibernate transaction is begun.
- afterTransactionCompletion(): Called after a transaction is committed or rolled back.
- beforeTransactionCompletion(): Called before a transaction is committed (but not before rollback).
- onCollectionRecreate(): Called before a collection is (re)created.
- onCollectionRemove(): Called before a collection is deleted.
- onCollectionUpdate(): Called before a collection is updated.
- onDelete(): Called before an object is deleted.
- onFlushDirty(): Called when an object is detected to be dirty, during a flush.
- onLoad(): Called just before an object is initialized.
- onSave(): Called before an object is saved.
- postFlush(): Called after a flush.
- preFlush(): Called before a flush.
Lets override onFlushDirty()
method from the Interceptor
interface. This method is invoked when an entity updates are flushed into the database. This method can help in identifying the changes in the entity in each flush operation.
In our example, we are simply logging the audit info in the logs. We can also insert this audit entry in the database if required. Basically, we can do a lot of things in these overridden methods based on business needs.
@Slf4j
public class AuditInterceptor implements Interceptor {
@Override
public boolean onFlushDirty(Object entity,
Object id,
Object[] currentState,
Object[] previousState,
String[] propertyNames,
Type[] types) throws CallbackException {
if (log.isDebugEnabled()) {
log.debug("********************AUDIT INFO START*******************");
log.debug("Entity Name :: " + entity.getClass());
log.debug("Previous state :: " + Arrays.deepToString(previousState));
log.debug("Current state :: " + Arrays.deepToString(currentState));
log.debug("propertyNames :: " + Arrays.deepToString(propertyNames));
log.debug("********************AUDIT INFO END*******************");
}
return Interceptor.super.onFlushDirty(entity,
id,
currentState,
previousState,
propertyNames,
types);
}
}
3. Registering Interceptor
We can register the Interceptor with the persistent context in two ways.
3.1. With Session
If we need to use the interceptor in only a couple of places in the application, we can register it with the Session
instances in those places.
try (Session session = sessionFactory.withOptions()
.interceptor(new AuditInterceptor()).openSession()) {
session.getTransaction().begin();
//...
}
3.2. With SessionFactory
To enable the interceptor in all the sessions created in the application, we can add the interceptor in the SessionFactory itself.
try {
StandardServiceRegistry standardRegistry
= new StandardServiceRegistryBuilder()
.configure("hibernate-test.cfg.xml")
.build();
Metadata metadata = new MetadataSources(standardRegistry)
.addAnnotatedClass(EmployeeEntity.class)
.getMetadataBuilder()
.build();
sessionFactory = metadata
.getSessionFactoryBuilder()
.applyInterceptor(new AuditInterceptor())
.build();
} catch (Throwable ex) {
throw new ExceptionInInitializerError(ex);
}
Make sure we do not store any kind of state information in the Interceptor because it will be shared by multiple threads. To make it even safer from accidental uses, we can make the session context to thread-local.
hibernate.current_session_context_class=org.hibernate.context.internal.ThreadLocalSessionContext
4. Demo
To demonstrate, let us create an entity and update its information.
@Test
public void testAuditLogging() {
try (Session session = sessionFactory.withOptions()
.interceptor(new AuditInterceptor()).openSession()) {
session.getTransaction().begin();
EmployeeEntity employee = new EmployeeEntity();
employee.setFirstName("Lokesh");
employee.setLastName("Gupta");
employee.setEmail("howtodoinjava@gmail.com");
//Save here
session.persist(employee);
session.flush();
//Update here
employee.setFirstName("Akash");
session.getTransaction().commit();
}
}
Notice the logs generated by Hibernate. Clearly, we are able to see the entity state before and after the flush.
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - ********************AUDIT INFO START*******************
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - Entity Name :: class com.howtodoinjava.basics.entity.EmployeeEntity
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - Previous state :: [howtodoinjava@gmail.com, Lokesh, Gupta]
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - Current state :: [howtodoinjava@gmail.com, Akash, Gupta]
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - propertyNames :: [email, firstName, lastName]
2022-05-10_11:11:28.662 DEBUG c.h.basics.entity.AuditInterceptor - ********************AUDIT INFO END*******************
5. Conclusion
In this tutorial, we learned to use the Hibernate Interceptor interface for getting callbacks on various persitence-related events. We learned to register the Interceptor with Session and SessionFactory interfaces.
Happy Learning !!
Leave a Reply