1. Overview

In this discussion, we’ll look at various ways of intercepting operations within Hibernate’s abstracted relational mapping implementation.

2. Defining Hibernate Interceptors

The Hibernate Interceptor is an interface that allows us to react to certain events within Hibernate.

These interceptors are registered as callbacks and provide communication links between Hibernate’s session and application. With such a callback, an application can intercept core Hibernate’s operations such as save, update, delete, etc.

There are two ways of defining interceptors:

  1. implementing the org.hibernate.Interceptor interface
  2. extending the org.hibernate.EmptyInterceptor class

2.1. Implementing an Interceptor Interface

Implementing org.hibernate.Interceptor requires implementing about 14 accompanying methods. These methods include onLoad, onSave, onDelete, findDirty, and a few more.

It’s also important to ensure that any class that implements Interceptor interface is serializable (implements java.io.Serializable).

A typical example would look like:

public class CustomInterceptorImpl implements Interceptor, Serializable {

    @Override
    public boolean onLoad(Object entity, Serializable id, 
      Object[] state, String[] propertyNames, Type[] types) 
      throws CallbackException {
        // ...
        return false;
    }

    // ...

    @Override
    public String onPrepareStatement(String sql) {
        // ...   
        return sql;
    }

}

If there are no special requirements, extending the EmptyInterceptor class and only overriding the required methods is highly recommended.

2.2. Extending EmptyInterceptor

Extending the org.hibernate.EmptyInterceptor class provides an easier way of defining an interceptor. We now only need to override the methods that relate to the operation we want to intercept.

For example, we can define our CustomInterceptor as:

public class CustomInterceptor extends EmptyInterceptor {
}

And if we need to intercept data saving operations before they are executed, we need to override onSave method:

@Override
public boolean onSave(Object entity, Serializable id, 
  Object[] state, String[] propertyNames, Type[] types) {
    
    if (entity instanceof User) {
        logger.info(((User) entity).toString());
    }
    return super.onSave(entity, id, state, propertyNames, types);
}

Notice how this implementation simply prints out the entity – if it’s a User.

While it’s possible to return a value of true or false, it’s a good practice to allow propagation of onSave event by invoking super.onSave().

Another use-case would be providing an audit trail for database interactions. We can use the onFlushDirty() method to know when an entity changes.

For the User object, we can decide to update its lastModified date property whenever changes on entities of type User happen.

This can be achieved with:

@Override
public boolean onFlushDirty(Object entity, Serializable id, 
  Object[] currentState, Object [] previousState, 
  String[] propertyNames, Type[] types) {
    
    if (entity instanceof User) {
        ((User) entity).setLastModified(new Date());
        logger.info(((User) entity).toString());
    }
    return super.onFlushDirty(entity, id, currentState, 
      previousState, propertyNames, types);
}

Other events such as delete and load (object initialization) can be intercepted by implementing the corresponding onDelete and onLoad methods respectively.

3. Registering Interceptors

A Hibernate interceptor can either be registered as Session-scoped or SessionFactory-scoped.

3.1. Session-scoped Interceptor

A Session-scoped interceptor is linked to a specific session. It’s created when the session is being defined or opened as:

public static Session getSessionWithInterceptor(Interceptor interceptor) 
  throws IOException {
    return getSessionFactory().withOptions()
      .interceptor(interceptor).openSession();
}

In the above, we explicitly registered an interceptor with a particular hibernate session.

3.2. SessionFactory-scoped Interceptor

A SessionFactory-scoped interceptor is registered before building a SessionFactory. This is typically done through the applyInterceptor method on a SessionFactoryBuilder instance:

ServiceRegistry serviceRegistry = configureServiceRegistry();
SessionFactory sessionFactory = getSessionFactoryBuilder(serviceRegistry)
  .applyInterceptor(new CustomInterceptor())
  .build();

It’s important to note that a SessionFactory-scoped interceptor will be applied to all sessions. Hence, we need to be careful not to store session specific state – as this interceptor will be used by different sessions concurrently.

For a session specific behavior, it’s recommended to explicitly open a session with a different interceptor as earlier shown.

For SessionFactory-scoped interceptors, we naturally need to ensure that it’s thread-safe. This can be achieved by specifying a session context in the properties file:

hibernate.current_session_context_class=org.hibernate.context.internal.ThreadLocalSessionContext

Or by adding this to our XML configuration file:

<property name="hibernate.current_session_context_class">
    org.hibernate.context.internal.ThreadLocalSessionContext
</property>

Also, to ensure serializability, SessionFactory-scoped interceptors must implement the readResolve method of the Serializable interface.

4. Conclusion

We’ve seen how to define and register Hibernate interceptors either as Session-scoped or SessionFactory-scoped. In either case, we must ensure that the interceptors are serializable especially if we want a serializable session.

Other alternatives to interceptors include Hibernate Events and JPA Callbacks.

And, as always, you can check out the complete source code over on Github.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.