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) {
// ...
}
}
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
StatelessSessionnever 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
UPDATEstatements — mutate only when the value actually needs to change, exactly as this chapter'suppercaseNameIfPresentchecks!upper.equals(s)before returningtrue. - Hibernate ORM 7.4 User Guide — interceptors