# 24 — Interceptors [← Previous: 23 — Pagination](23-pagination.md) | [Next: 25 — Hibernate Search →](25-hibernate-search.md) | [Back to README →](../README.md) 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: ```java public class UppercasingInterceptor implements Interceptor { @Override public boolean onFlushDirty(Object entity, Object id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { // ... } } ``` [`UppercasingInterceptor.java`](../src/main/java/com/ankurm/hibernatedemo/interceptor/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 ```java 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](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) — [output](output/24-session-scoped-mutation.txt) ## Session-scoped vs. globally-registered interceptors ```java sessionFactory.withOptions().interceptor(myInterceptor).openSession(); ``` scopes the interceptor to that one `Session` — a plain `sessionFactory.openSession()` elsewhere is completely unaffected. [Test](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) — [output](output/24-interceptor-scoping.txt) 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)`: ```java 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](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) — [output](output/24-global-via-property.txt) ## Bulk HQL updates bypass interceptor callbacks entirely ```java 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](../src/test/java/com/ankurm/hibernatedemo/interceptor/InterceptorTest.java) — [output](output/24-bulk-update-bypass.txt) ## 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](https://docs.jboss.org/hibernate/orm/7.4/userguide/html_single/Hibernate_User_Guide.html#events-interceptors)