14. Interceptors and events

It is useful for the application to react to certain events that occur inside Hibernate. This allows for the implementation of generic functionality and the extension of Hibernate functionality.

14.1. Interceptors

The org.hibernate.Interceptor interface provides callbacks from the session to the application, allowing the application to inspect and/or manipulate properties of a persistent object before it is saved, updated, deleted or loaded.

One possible use for this is to track auditing information. The following example shows an Interceptor implementation that automatically logs when an entity is updated.

  1. public static class LoggingInterceptor extends EmptyInterceptor {
  2. @Override
  3. public boolean onFlushDirty(
  4. Object entity,
  5. Serializable id,
  6. Object[] currentState,
  7. Object[] previousState,
  8. String[] propertyNames,
  9. Type[] types) {
  10. LOGGER.debugv( "Entity {0}#{1} changed from {2} to {3}",
  11. entity.getClass().getSimpleName(),
  12. id,
  13. Arrays.toString( previousState ),
  14. Arrays.toString( currentState )
  15. );
  16. return super.onFlushDirty( entity, id, currentState,
  17. previousState, propertyNames, types
  18. );
  19. }
  20. }

You can either implement Interceptor directly or extend the org.hibernate.EmptyInterceptor base class.

An Interceptor can be either Session-scoped or SessionFactory-scoped.

A Session-scoped interceptor is specified when a session is opened.

  1. SessionFactory sessionFactory = entityManagerFactory.unwrap( SessionFactory.class );
  2. Session session = sessionFactory
  3. .withOptions()
  4. .interceptor(new LoggingInterceptor() )
  5. .openSession();
  6. session.getTransaction().begin();
  7. Customer customer = session.get( Customer.class, customerId );
  8. customer.setName( "Mr. John Doe" );
  9. //Entity Customer#1 changed from [John Doe, 0] to [Mr. John Doe, 0]
  10. session.getTransaction().commit();

A SessionFactory-scoped interceptor is registered with the Configuration object prior to building the SessionFactory. Unless a session is opened explicitly specifying the interceptor to use, the SessionFactory-scoped interceptor will be applied to all sessions opened from that SessionFactory. SessionFactory-scoped interceptors must be thread-safe. Ensure that you do not store session-specific states since multiple sessions will use this interceptor potentially concurrently.

  1. SessionFactory sessionFactory = new MetadataSources( new StandardServiceRegistryBuilder().build() )
  2. .addAnnotatedClass( Customer.class )
  3. .getMetadataBuilder()
  4. .build()
  5. .getSessionFactoryBuilder()
  6. .applyInterceptor( new LoggingInterceptor() )
  7. .build();

14.2. Native Event system

If you have to react to particular events in the persistence layer, you can also use the Hibernate event architecture. The event system can be used in place of or in addition to interceptors.

Many methods of the Session interface correlate to an event type. The full range of defined event types is declared as enum values on org.hibernate.event.spi.EventType. When a request is made of one of these methods, the Session generates an appropriate event and passes it to the configured event listener(s) for that type.

Applications can customize the listener interfaces (i.e., the LoadEvent is processed by the registered implementation of the LoadEventListener interface), in which case their implementations would be responsible for processing the load() requests made of the Session.

The listeners should be considered stateless. They are shared between requests, and should not save any state as instance variables.

A custom listener implements the appropriate interface for the event it wants to process and/or extend one of the convenience base classes (or even the default event listeners used by Hibernate out-of-the-box as these are declared non-final for this purpose).

Here is an example of a custom load event listener:

Example 475. Custom LoadListener example

  1. EntityManagerFactory entityManagerFactory = entityManagerFactory();
  2. SessionFactoryImplementor sessionFactory = entityManagerFactory.unwrap( SessionFactoryImplementor.class );
  3. sessionFactory
  4. .getServiceRegistry()
  5. .getService( EventListenerRegistry.class )
  6. .prependListeners( EventType.LOAD, new SecuredLoadEntityListener() );
  7. Customer customer = entityManager.find( Customer.class, customerId );

14.3. Mixing Events and Interceptors

When you want to customize the entity state transition behavior, you have two options:

  1. you provide a custom Interceptor, which is taken into consideration by the default Hibernate event listeners. For example, the Interceptor#onSave() method is invoked by Hibernate AbstractSaveEventListener. Or, the Interceptor#onLoad() is called by the DefaultPreLoadEventListener.

  2. you can replace any given default event listener with your own implementation. When doing this, you should probably extend the default listeners because otherwise, you’d have to take care of all the low-level entity state transition logic. For example, if you replace the DefaultPreLoadEventListener with your own implementation, then, only if you call the Interceptor#onLoad() method explicitly, you can mix the custom load event listener with a custom Hibernate interceptor.

14.4. Hibernate declarative security

Usually, declarative security in Hibernate applications is managed in a session facade layer. Hibernate allows certain actions to be authorized via JACC and JAAS. This is an optional functionality that is built on top of the event architecture.

First, you must configure the appropriate event listeners, to enable the use of JACC authorization. Again, see Event Listener Registration for the details.

Below is an example of an appropriate org.hibernate.integrator.spi.Integrator implementation for this purpose.

Example 476. JACC listener registration example

  1. public static class JaccIntegrator implements ServiceContributingIntegrator {
  2. private static final Logger log = Logger.getLogger( JaccIntegrator.class );
  3. private static final DuplicationStrategy DUPLICATION_STRATEGY =
  4. new DuplicationStrategy() {
  5. @Override
  6. public boolean areMatch(Object listener, Object original) {
  7. return listener.getClass().equals( original.getClass() ) &&
  8. JaccSecurityListener.class.isInstance( original );
  9. }
  10. @Override
  11. public Action getAction() {
  12. return Action.KEEP_ORIGINAL;
  13. }
  14. };
  15. @Override
  16. public void prepareServices(
  17. StandardServiceRegistryBuilder serviceRegistryBuilder) {
  18. boolean isSecurityEnabled = serviceRegistryBuilder
  19. .getSettings().containsKey( AvailableSettings.JACC_ENABLED );
  20. final JaccService jaccService = isSecurityEnabled ?
  21. new StandardJaccServiceImpl() : new DisabledJaccServiceImpl();
  22. serviceRegistryBuilder.addService( JaccService.class, jaccService );
  23. }
  24. @Override
  25. public void integrate(
  26. Metadata metadata,
  27. SessionFactoryImplementor sessionFactory,
  28. SessionFactoryServiceRegistry serviceRegistry) {
  29. doIntegration(
  30. serviceRegistry
  31. .getService( ConfigurationService.class ).getSettings(),
  32. // pass no permissions here, because atm actually injecting the
  33. // permissions into the JaccService is handled on SessionFactoryImpl via
  34. // the org.hibernate.boot.cfgxml.spi.CfgXmlAccessService
  35. null,
  36. serviceRegistry
  37. );
  38. }
  39. private void doIntegration(
  40. Map properties,
  41. JaccPermissionDeclarations permissionDeclarations,
  42. SessionFactoryServiceRegistry serviceRegistry) {
  43. boolean isSecurityEnabled = properties
  44. .containsKey( AvailableSettings.JACC_ENABLED );
  45. if ( ! isSecurityEnabled ) {
  46. log.debug( "Skipping JACC integration as it was not enabled" );
  47. return;
  48. }
  49. final String contextId = (String) properties
  50. .get( AvailableSettings.JACC_CONTEXT_ID );
  51. if ( contextId == null ) {
  52. throw new IntegrationException( "JACC context id must be specified" );
  53. }
  54. final JaccService jaccService = serviceRegistry
  55. .getService( JaccService.class );
  56. if ( jaccService == null ) {
  57. throw new IntegrationException( "JaccService was not set up" );
  58. }
  59. if ( permissionDeclarations != null ) {
  60. for ( GrantedPermission declaration : permissionDeclarations
  61. .getPermissionDeclarations() ) {
  62. jaccService.addPermission( declaration );
  63. }
  64. }
  65. final EventListenerRegistry eventListenerRegistry =
  66. serviceRegistry.getService( EventListenerRegistry.class );
  67. eventListenerRegistry.addDuplicationStrategy( DUPLICATION_STRATEGY );
  68. eventListenerRegistry.prependListeners(
  69. EventType.PRE_DELETE, new JaccPreDeleteEventListener() );
  70. eventListenerRegistry.prependListeners(
  71. EventType.PRE_INSERT, new JaccPreInsertEventListener() );
  72. eventListenerRegistry.prependListeners(
  73. EventType.PRE_UPDATE, new JaccPreUpdateEventListener() );
  74. eventListenerRegistry.prependListeners(
  75. EventType.PRE_LOAD, new JaccPreLoadEventListener() );
  76. }
  77. @Override
  78. public void disintegrate(SessionFactoryImplementor sessionFactory,
  79. SessionFactoryServiceRegistry serviceRegistry) {
  80. // nothing to do
  81. }
  82. }

You must also decide how to configure your JACC provider. Consult your JACC provider documentation.

14.5. JPA Callbacks

JPA also defines a more limited set of callbacks through annotations.

Table 8. Callback annotations
TypeDescription

@PrePersist

Executed before the entity manager persist operation is actually executed or cascaded. This call is synchronous with the persist operation.

@PreRemove

Executed before the entity manager remove operation is actually executed or cascaded. This call is synchronous with the remove operation.

@PostPersist

Executed after the entity manager persist operation is actually executed or cascaded. This call is invoked after the database INSERT is executed.

@PostRemove

Executed after the entity manager remove operation is actually executed or cascaded. This call is synchronous with the remove operation.

@PreUpdate

Executed before the database UPDATE operation.

@PostUpdate

Executed after the database UPDATE operation.

@PostLoad

Executed after an entity has been loaded into the current persistence context or an entity has been refreshed.

There are two available approaches defined for specifying callback handling:

  • The first approach is to annotate methods on the entity itself to receive notifications of a particular entity lifecycle event(s).

  • The second is to use a separate entity listener class. An entity listener is a stateless class with a no-arg constructor. The callback annotations are placed on a method of this class instead of the entity class. The entity listener class is then associated with the entity using the javax.persistence.EntityListeners annotation

Example 477. Example of specifying JPA callbacks

  1. @Entity
  2. @EntityListeners( LastUpdateListener.class )
  3. public static class Person {
  4. @Id
  5. private Long id;
  6. private String name;
  7. private Date dateOfBirth;
  8. @Transient
  9. private long age;
  10. private Date lastUpdate;
  11. public void setLastUpdate(Date lastUpdate) {
  12. this.lastUpdate = lastUpdate;
  13. }
  14. /**
  15. * Set the transient property at load time based on a calculation.
  16. * Note that a native Hibernate formula mapping is better for this purpose.
  17. */
  18. @PostLoad
  19. public void calculateAge() {
  20. age = ChronoUnit.YEARS.between( LocalDateTime.ofInstant(
  21. Instant.ofEpochMilli( dateOfBirth.getTime()), ZoneOffset.UTC),
  22. LocalDateTime.now()
  23. );
  24. }
  25. }
  26. public static class LastUpdateListener {
  27. @PreUpdate
  28. @PrePersist
  29. public void setLastUpdate( Person p ) {
  30. p.setLastUpdate( new Date() );
  31. }
  32. }

These approaches can be mixed, meaning you can use both together.

Regardless of whether the callback method is defined on the entity or on an entity listener, it must have a void-return signature. The name of the method is irrelevant as it is the placement of the callback annotations that makes the method a callback. In the case of callback methods defined on the entity class, the method must additionally have a no-argument signature. For callback methods defined on an entity listener class, the method must have a single argument signature; the type of that argument can be either java.lang.Object (to facilitate attachment to multiple entities) or the specific entity type.

A callback method can throw a RuntimeException. If the callback method does throw a RuntimeException, then the current transaction, if any, must be rolled back.

A callback method must not invoke EntityManager or Query methods!

It is possible that multiple callback methods are defined for a particular lifecycle event. When that is the case, the defined order of execution is well defined by the JPA spec (specifically section 3.5.4):

  • Any default listeners associated with the entity are invoked first, in the order they were specified in the XML. See the javax.persistence.ExcludeDefaultListeners annotation.

  • Next, entity listener class callbacks associated with the entity hierarchy are invoked, in the order they are defined in the EntityListeners. If multiple classes in the entity hierarchy define entity listeners, the listeners defined for a superclass are invoked before the listeners defined for its subclasses. See the javax.persistence.ExcludeSuperclassListener‘s annotation.

  • Lastly, callback methods defined on the entity hierarchy are invoked. If a callback type is annotated on both an entity and one or more of its superclasses without method overriding, both would be called, the most general superclass first. An entity class is also allowed to override a callback method defined in a superclass in which case the super callback would not get invoked; the overriding method would get invoked provided it is annotated.

14.6. Default entity listeners

The JPA specification allows you to define a default entity listener which is going to be applied for every entity in that particular system. Default entity listeners can only be defined in XML mapping files.

Example 478. Default event listener mapping

  1. public class DefaultEntityListener {
  2. public void onPersist(Object entity) {
  3. if ( entity instanceof BaseEntity ) {
  4. BaseEntity baseEntity = (BaseEntity) entity;
  5. baseEntity.setCreatedOn( now() );
  6. }
  7. }
  8. public void onUpdate(Object entity) {
  9. if ( entity instanceof BaseEntity ) {
  10. BaseEntity baseEntity = (BaseEntity) entity;
  11. baseEntity.setUpdatedOn( now() );
  12. }
  13. }
  14. private Timestamp now() {
  15. return Timestamp.from(
  16. LocalDateTime.now().toInstant( ZoneOffset.UTC )
  17. );
  18. }
  19. }
  1. <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
  4. http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
  5. version="2.1">
  6. <persistence-unit-metadata>
  7. <persistence-unit-defaults>
  8. <entity-listeners>
  9. <entity-listener
  10. class="org.hibernate.userguide.events.DefaultEntityListener">
  11. <pre-persist method-name="onPersist"/>
  12. <pre-update method-name="onUpdate"/>
  13. </entity-listener>
  14. </entity-listeners>
  15. </persistence-unit-defaults>
  16. </persistence-unit-metadata>
  17. </entity-mappings>

Considering that all entities extend the BaseEntity class:

  1. @MappedSuperclass
  2. public abstract class BaseEntity {
  3. private Timestamp createdOn;
  4. private Timestamp updatedOn;
  5. //Getters and setters are omitted for brevity
  6. }
  1. @Entity(name = "Person")
  2. public static class Person extends BaseEntity {
  3. @Id
  4. private Long id;
  5. private String name;
  6. //Getters and setters omitted for brevity
  7. }
  8. @Entity(name = "Book")
  9. public static class Book extends BaseEntity {
  10. @Id
  11. private Long id;
  12. private String title;
  13. @ManyToOne
  14. private Person author;
  15. //Getters and setters omitted for brevity
  16. }

When persisting a Person or Book entity, the createdOn is going to be set by the onPersist method of the DefaultEntityListener.

Example 479. Default event listener persist event

  1. Person author = new Person();
  2. author.setId( 1L );
  3. author.setName( "Vlad Mihalcea" );
  4. entityManager.persist( author );
  5. Book book = new Book();
  6. book.setId( 1L );
  7. book.setTitle( "High-Performance Java Persistence" );
  8. book.setAuthor( author );
  9. entityManager.persist( book );
  1. insert
  2. into
  3. Person
  4. (createdOn, updatedOn, name, id)
  5. values
  6. (?, ?, ?, ?)
  7. -- binding parameter [1] as [TIMESTAMP] - [2017-06-08 19:23:48.224]
  8. -- binding parameter [2] as [TIMESTAMP] - [null]
  9. -- binding parameter [3] as [VARCHAR] - [Vlad Mihalcea]
  10. -- binding parameter [4] as [BIGINT] - [1]
  11. insert
  12. into
  13. Book
  14. (createdOn, updatedOn, author_id, title, id)
  15. values
  16. (?, ?, ?, ?, ?)
  17. -- binding parameter [1] as [TIMESTAMP] - [2017-06-08 19:23:48.246]
  18. -- binding parameter [2] as [TIMESTAMP] - [null]
  19. -- binding parameter [3] as [BIGINT] - [1]
  20. -- binding parameter [4] as [VARCHAR] - [High-Performance Java Persistence]
  21. -- binding parameter [5] as [BIGINT] - [1]

When updating a Person or Book entity, the updatedOn is going to be set by the onUpdate method of the DefaultEntityListener.

Example 480. Default event listener update event

  1. Person author = entityManager.find( Person.class, 1L );
  2. author.setName( "Vlad-Alexandru Mihalcea" );
  3. Book book = entityManager.find( Book.class, 1L );
  4. book.setTitle( "High-Performance Java Persistence 2nd Edition" );
  1. update
  2. Person
  3. set
  4. createdOn=?,
  5. updatedOn=?,
  6. name=?
  7. where
  8. id=?
  9. -- binding parameter [1] as [TIMESTAMP] - [2017-06-08 19:23:48.224]
  10. -- binding parameter [2] as [TIMESTAMP] - [2017-06-08 19:23:48.316]
  11. -- binding parameter [3] as [VARCHAR] - [Vlad-Alexandru Mihalcea]
  12. -- binding parameter [4] as [BIGINT] - [1]
  13. update
  14. Book
  15. set
  16. createdOn=?,
  17. updatedOn=?,
  18. author_id=?,
  19. title=?
  20. where
  21. id=?
  22. -- binding parameter [1] as [TIMESTAMP] - [2017-06-08 19:23:48.246]
  23. -- binding parameter [2] as [TIMESTAMP] - [2017-06-08 19:23:48.317]
  24. -- binding parameter [3] as [BIGINT] - [1]
  25. -- binding parameter [4] as [VARCHAR] - [High-Performance Java Persistence 2nd Edition]
  26. -- binding parameter [5] as [BIGINT] - [1]

14.6.1. Exclude default entity listeners

If you already registered a default entity listener, but you don’t want to apply it to a particular entity, you can use the @ExcludeDefaultListeners and @ExcludeSuperclassListeners JPA annotations.

@ExcludeDefaultListeners instructs the current class to ignore the default entity listeners for the current entity while @ExcludeSuperclassListeners is used to ignore the default entity listeners propagated to the BaseEntity super-class.

Example 481. Exclude default event listener mapping

  1. @Entity(name = "Publisher")
  2. @ExcludeDefaultListeners
  3. @ExcludeSuperclassListeners
  4. public static class Publisher extends BaseEntity {
  5. @Id
  6. private Long id;
  7. private String name;
  8. //Getters and setters omitted for brevity
  9. }

When persisting a Publisher entity, the createdOn is not going to be set by the onPersist method of the DefaultEntityListener because the Publisher entity was marked with the @ExcludeDefaultListeners and @ExcludeSuperclassListeners annotations.

Example 482. Excluding default event listener events

  1. Publisher publisher = new Publisher();
  2. publisher.setId( 1L );
  3. publisher.setName( "Amazon" );
  4. entityManager.persist( publisher );
  1. insert
  2. into
  3. Publisher
  4. (createdOn, updatedOn, name, id)
  5. values
  6. (?, ?, ?, ?)
  7. -- binding parameter [1] as [TIMESTAMP] - [null]
  8. -- binding parameter [2] as [TIMESTAMP] - [null]
  9. -- binding parameter [3] as [VARCHAR] - [Amazon]
  10. -- binding parameter [4] as [BIGINT] - [1]