Files
hibernate-demo/docs/24-interceptors.md
T

5.4 KiB

24 — Interceptors

← Previous: 23 — Pagination | Next: 25 — Hibernate Search → | Back to README →

Backs the rewrite of ankurm.com post 4891 (interceptors).

Interceptor is all default methods now — extending EmptyInterceptor is obsolete

javap against org.hibernate.Interceptor in hibernate-core-7.4.5.Final.jar shows every method (onSave, onFlushDirty, onDelete, onLoad, findDirty, and the rest) declared default. There is no longer a reason to extend a no-op base class just to override one or two callbacks — implement Interceptor directly:

public class UppercasingInterceptor implements Interceptor {
    @Override
    public boolean onFlushDirty(Object entity, Object id, Object[] currentState,
            Object[] previousState, String[] propertyNames, Type[] types) {
        // ...
    }
}

UppercasingInterceptor.java

The old public org.hibernate.EmptyInterceptor base class still exists in the 7.4.5.Final jar — but only as org.hibernate.internal.EmptyInterceptor: a final, singleton-only class (public static final Interceptor INSTANCE) that isn't meant to be extended by application code any more. It moved from a public API class to an internal implementation detail, confirmed by javap, not by assuming it was simply deleted.

Also note the identifier parameter type: onSave/onFlushDirty/onDelete/onLoad all take Object id, not java.io.Serializable id — Hibernate 6 widened this across the whole interface, since an application is free to use a non-Serializable identifier type.

The state-array-mutation contract

private boolean uppercaseNameIfPresent(Object[] state, String[] propertyNames) {
    for (int i = 0; i < propertyNames.length; i++) {
        if ("name".equals(propertyNames[i]) && state[i] instanceof String s) {
            String upper = s.toUpperCase(Locale.ROOT);
            if (!upper.equals(s)) {
                state[i] = upper;
                return true; // tells Hibernate: yes, I changed the state array, flush it
            }
        }
    }
    return false;
}

The boolean return value is the contract: true tells Hibernate the state array was actually mutated, so the (possibly changed) values get flushed to the database; false (or returning without touching state) leaves the original values untouched. Look the property up by name in propertyNames — the array's index order is Hibernate's internal property ordering, not necessarily the entity's declaration order. Test — output

Session-scoped vs. globally-registered interceptors

sessionFactory.withOptions().interceptor(myInterceptor).openSession();

scopes the interceptor to that one Session — a plain sessionFactory.openSession() elsewhere is completely unaffected. Test — output

To register one for every Session a SessionFactory ever opens, set hibernate.session_factory.interceptor once, at SessionFactory build time — this is exactly the property a Spring Boot HibernatePropertiesCustomizer bean sets under the hood when it calls properties.put("hibernate.session_factory.interceptor", interceptor):

new StandardServiceRegistryBuilder()
        .applySetting("hibernate.session_factory.interceptor", globalInterceptor)
        .build();

Demonstrated here on a standalone, non-Spring registry deliberately — registering a global interceptor on this repo's shared Spring-managed SessionFactory would retroactively affect every other chapter's tests that reuse the same cached Spring context. Test — output

Bulk HQL updates bypass interceptor callbacks entirely

session.createMutationQuery("update Task set name = 'renamed by bulk update' where id = :id")
        .setParameter("id", id)
        .executeUpdate();

onFlushDirty never fires for this. A bulk HQL (or native SQL) mutation changes rows directly in the database via a single UPDATE/DELETE statement — it never loads a managed entity instance into the persistence context, and onFlushDirty needs a managed entity's dirty state to fire against in the first place. Anything an interceptor is relied on for (auditing, denormalized field maintenance) has to be handled separately for bulk operations. Test — output

Going deeper

  • StatelessSession never calls interceptor callbacks at all, by design — it exists specifically to skip persistence-context machinery for bulk-style work.
  • An interceptor that mutates unrelated fields on every flush is a subtle source of extra UPDATE statements — mutate only when the value actually needs to change, exactly as this chapter's uppercaseNameIfPresent checks !upper.equals(s) before returning true.
  • Hibernate ORM 7.4 User Guide — interceptors