Introduction
Hibernate Interceptors and Events provide powerful hooks into Hibernate's persistence lifecycle. They allow you to implement cross-cutting concerns like auditing, logging, validation, and custom business logic that executes during database operations.
Hibernate Interceptors
Basic Interceptor Implementation
public class CustomInterceptor extends EmptyInterceptor {
private static final Logger logger = LoggerFactory.getLogger(CustomInterceptor.class);
@Override
public boolean onSave(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
logger.info("onSave called for entity: {} with id: {}", entity.getClass().getSimpleName(), id);
if (entity instanceof Auditable) {
setCurrentTimestamp(state, propertyNames, "createdAt");
}
return false; // state was not modified
}
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
Object[] previousState, String[] propertyNames, Type[] types) {
logger.info("onFlushDirty called for entity: {} with id: {}", entity.getClass().getSimpleName(), id);
if (entity instanceof Auditable) {
setCurrentTimestamp(currentState, propertyNames, "updatedAt");
return true; // state was modified
}
return false;
}
@Override
public void onDelete(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
logger.info("onDelete called for entity: {} with id: {}", entity.getClass().getSimpleName(), id);
if (entity instanceof SoftDeletable) {
logger.warn("Attempting to delete soft-deletable entity: {}", id);
}
}
@Override
public String onPrepareStatement(String sql) {
logger.debug("Executing SQL: {}", sql);
return super.onPrepareStatement(sql);
}
private void setCurrentTimestamp(Object[] state, String[] propertyNames, String propertyName) {
for (int i = 0; i < propertyNames.length; i++) {
if (propertyNames[i].equals(propertyName)) {
state[i] = Instant.now();
break;
}
}
}
}
// Marker interfaces
interface Auditable {}
interface SoftDeletable {}
Advanced Interceptor with Dependency Injection
@Component
public class AdvancedInterceptor extends EmptyInterceptor {
private final AuditService auditService;
private final SecurityContext securityContext;
// ThreadLocal to store session-specific data
private static final ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>();
public AdvancedInterceptor(AuditService auditService, SecurityContext securityContext) {
this.auditService = auditService;
this.securityContext = securityContext;
}
@Override
public boolean onSave(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
if (entity instanceof AuditableEntity) {
setAuditFields(state, propertyNames, "CREATE");
auditService.logCreation(entity, getCurrentUser());
}
validateEntity(entity);
return super.onSave(entity, id, state, propertyNames, types);
}
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState,
Object[] previousState, String[] propertyNames, Type[] types) {
if (entity instanceof AuditableEntity) {
setAuditFields(currentState, propertyNames, "UPDATE");
// Create audit trail of changes
Map<String, ChangeDetail> changes = detectChanges(previousState, currentState, propertyNames);
auditService.logUpdate(entity, changes, getCurrentUser());
}
validateEntity(entity);
return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
}
@Override
public void onDelete(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
if (entity instanceof AuditableEntity) {
auditService.logDeletion(entity, getCurrentUser());
}
if (entity instanceof SoftDeletableEntity) {
handleSoftDelete(entity, state, propertyNames);
}
super.onDelete(entity, id, state, propertyNames, types);
}
@Override
public void afterTransactionCompletion(Transaction tx) {
if (tx.wasCommitted()) {
logger.info("Transaction committed successfully");
} else {
logger.warn("Transaction rolled back");
}
cleanupSessionContext();
}
private void setAuditFields(Object[] state, String[] propertyNames, String operation) {
String user = getCurrentUser();
Instant now = Instant.now();
for (int i = 0; i < propertyNames.length; i++) {
switch (propertyNames[i]) {
case "createdBy" -> {
if ("CREATE".equals(operation)) {
state[i] = user;
}
}
case "createdAt" -> {
if ("CREATE".equals(operation)) {
state[i] = now;
}
}
case "updatedBy" -> state[i] = user;
case "updatedAt" -> state[i] = now;
}
}
}
private Map<String, ChangeDetail> detectChanges(Object[] oldState, Object[] newState, String[] propertyNames) {
Map<String, ChangeDetail> changes = new HashMap<>();
for (int i = 0; i < propertyNames.length; i++) {
if (!Objects.equals(oldState[i], newState[i])) {
changes.put(propertyNames[i], new ChangeDetail(oldState[i], newState[i]));
}
}
return changes;
}
private void validateEntity(Object entity) {
if (entity instanceof Validatable validatable) {
Set<ConstraintViolation<Validatable>> violations = validatable.validate();
if (!violations.isEmpty()) {
throw new ValidationException("Entity validation failed", violations);
}
}
}
private void handleSoftDelete(Object entity, Object[] state, String[] propertyNames) {
for (int i = 0; i < propertyNames.length; i++) {
if ("deleted".equals(propertyNames[i])) {
state[i] = true;
break;
}
}
// Don't actually delete - this will prevent the delete operation
throw new SoftDeleteException("Entity soft-deleted instead of physically removed");
}
private String getCurrentUser() {
return securityContext.getCurrentUser().orElse("system");
}
private void cleanupSessionContext() {
sessionContext.remove();
}
// Supporting classes
public static class ChangeDetail {
private final Object oldValue;
private final Object newValue;
public ChangeDetail(Object oldValue, Object newValue) {
this.oldValue = oldValue;
this.newValue = newValue;
}
// getters
}
public static class SessionContext {
private String sessionId;
private String userAgent;
private String ipAddress;
// ... other session data
}
}
// Custom exceptions
class ValidationException extends RuntimeException {
private final Set<?> violations;
public ValidationException(String message, Set<?> violations) {
super(message);
this.violations = violations;
}
}
class SoftDeleteException extends RuntimeException {
public SoftDeleteException(String message) {
super(message);
}
}
Hibernate Event System
Event Listener Registration
@Configuration
public class HibernateEventConfig {
@Autowired
private ApplicationContext applicationContext;
@Bean
public LocalSessionFactoryBean sessionFactory(DataSource dataSource) {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setPackagesToScan("com.example.entity");
sessionFactory.setHibernateProperties(hibernateProperties());
// Register event listeners
Map<String, Object> eventListeners = new HashMap<>();
eventListeners.put("pre-insert", applicationContext.getBean(PreInsertEventListener.class));
eventListeners.put("pre-update", applicationContext.getBean(PreUpdateEventListener.class));
eventListeners.put("pre-delete", applicationContext.getBean(PreDeleteEventListener.class));
eventListeners.put("post-insert", applicationContext.getBean(PostInsertEventListener.class));
eventListeners.put("post-update", applicationContext.getBean(PostUpdateEventListener.class));
eventListeners.put("post-delete", applicationContext.getBean(PostDeleteEventListener.class));
eventListeners.put("pre-load", applicationContext.getBean(PreLoadEventListener.class));
sessionFactory.setHibernateProperties(eventListeners);
return sessionFactory;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.put("hibernate.show_sql", "true");
properties.put("hibernate.format_sql", "true");
return properties;
}
}
Comprehensive Event Listeners
@Component
public class AuditEventListener implements
PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener {
private static final Logger logger = LoggerFactory.getLogger(AuditEventListener.class);
private final AuditTrailService auditTrailService;
private final SecurityService securityService;
public AuditEventListener(AuditTrailService auditTrailService, SecurityService securityService) {
this.auditTrailService = auditTrailService;
this.securityService = securityService;
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
Object entity = event.getEntity();
if (entity instanceof Auditable) {
setAuditFields(event.getState(), event.getPersister().getPropertyNames(), "CREATE");
auditTrailService.recordEvent(entity, AuditEventType.CREATE, getCurrentUser());
}
logger.info("Pre-insert for entity: {}", entity.getClass().getSimpleName());
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
Object entity = event.getEntity();
if (entity instanceof Auditable) {
setAuditFields(event.getState(), event.getPersister().getPropertyNames(), "UPDATE");
// Detect changes for audit trail
Object[] oldState = event.getOldState();
Object[] newState = event.getState();
String[] propertyNames = event.getPersister().getPropertyNames();
Map<String, Object> changes = computeChanges(oldState, newState, propertyNames);
auditTrailService.recordEvent(entity, AuditEventType.UPDATE, getCurrentUser(), changes);
}
logger.info("Pre-update for entity: {}", entity.getClass().getSimpleName());
return false;
}
@Override
public boolean onPreDelete(PreDeleteEvent event) {
Object entity = event.getEntity();
if (entity instanceof Auditable) {
auditTrailService.recordEvent(entity, AuditEventType.DELETE, getCurrentUser());
}
if (entity instanceof SoftDeletable) {
logger.warn("Attempt to delete soft-deletable entity: {}", entity);
// Could throw exception to prevent deletion
}
logger.info("Pre-delete for entity: {}", entity.getClass().getSimpleName());
return false;
}
private void setAuditFields(Object[] state, String[] propertyNames, String operation) {
String user = getCurrentUser();
Instant now = Instant.now();
for (int i = 0; i < propertyNames.length; i++) {
switch (propertyNames[i]) {
case "createdBy" -> {
if ("CREATE".equals(operation)) {
state[i] = user;
}
}
case "createdAt" -> {
if ("CREATE".equals(operation)) {
state[i] = now;
}
}
case "updatedBy" -> state[i] = user;
case "updatedAt" -> state[i] = now;
}
}
}
private Map<String, Object> computeChanges(Object[] oldState, Object[] newState, String[] propertyNames) {
Map<String, Object> changes = new HashMap<>();
for (int i = 0; i < propertyNames.length; i++) {
if (!Objects.equals(oldState[i], newState[i])) {
changes.put(propertyNames[i], Map.of(
"old", oldState[i],
"new", newState[i]
));
}
}
return changes;
}
private String getCurrentUser() {
return securityService.getCurrentUsername().orElse("system");
}
}
@Component
public class ValidationEventListener implements PreInsertEventListener, PreUpdateEventListener {
private final Validator validator;
public ValidationEventListener(Validator validator) {
this.validator = validator;
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
validateEntity(event.getEntity());
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
validateEntity(event.getEntity());
return false;
}
private void validateEntity(Object entity) {
Set<ConstraintViolation<Object>> violations = validator.validate(entity);
if (!violations.isEmpty()) {
StringBuilder message = new StringBuilder("Validation failed for ")
.append(entity.getClass().getSimpleName())
.append(": ");
for (ConstraintViolation<Object> violation : violations) {
message.append(violation.getPropertyPath())
.append(" ")
.append(violation.getMessage())
.append("; ");
}
throw new ConstraintViolationException(message.toString(), violations);
}
}
}
@Component
public class PostOperationEventListener implements
PostInsertEventListener, PostUpdateEventListener, PostDeleteEventListener {
private static final Logger logger = LoggerFactory.getLogger(PostOperationEventListener.class);
private final CacheEvictionService cacheEvictionService;
private final MetricsService metricsService;
public PostOperationEventListener(CacheEvictionService cacheEvictionService,
MetricsService metricsService) {
this.cacheEvictionService = cacheEvictionService;
this.metricsService = metricsService;
}
@Override
public void onPostInsert(PostInsertEvent event) {
Object entity = event.getEntity();
logger.info("Post-insert for entity: {}", entity.getClass().getSimpleName());
// Evict relevant caches
cacheEvictionService.evictEntityCaches(entity);
// Record metrics
metricsService.recordEntityOperation(entity.getClass().getSimpleName(), "INSERT");
}
@Override
public void onPostUpdate(PostUpdateEvent event) {
Object entity = event.getEntity();
logger.info("Post-update for entity: {}", entity.getClass().getSimpleName());
// Evict relevant caches
cacheEvictionService.evictEntityCaches(entity);
// Record metrics
metricsService.recordEntityOperation(entity.getClass().getSimpleName(), "UPDATE");
}
@Override
public void onPostDelete(PostDeleteEvent event) {
Object entity = event.getEntity();
logger.info("Post-delete for entity: {}", entity.getClass().getSimpleName());
// Evict relevant caches
cacheEvictionService.evictEntityCaches(entity);
// Record metrics
metricsService.recordEntityOperation(entity.getClass().getSimpleName(), "DELETE");
}
@Override
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
}
@Component
public class PreLoadEventListener implements org.hibernate.event.spi.PreLoadEventListener {
private final SecurityService securityService;
private final DataFilterService dataFilterService;
public PreLoadEventListener(SecurityService securityService, DataFilterService dataFilterService) {
this.securityService = securityService;
this.dataFilterService = dataFilterService;
}
@Override
public void onPreLoad(PreLoadEvent event) {
Object entity = event.getEntity();
// Apply security filters
if (entity instanceof SecuredEntity securedEntity) {
if (!dataFilterService.canAccess(securedEntity, securityService.getCurrentUser())) {
throw new SecurityException("Access denied to entity: " + entity.getClass().getSimpleName());
}
}
// Apply tenant filtering for multi-tenant applications
if (entity instanceof TenantAware tenantAware) {
String currentTenant = securityService.getCurrentTenant();
if (!tenantAware.getTenantId().equals(currentTenant)) {
throw new SecurityException("Cross-tenant access attempted");
}
}
logger.debug("Pre-load for entity: {}", entity.getClass().getSimpleName());
}
}
Entity Classes with Interceptor/Event Support
Advanced Entity Examples
@Entity
@Table(name = "users")
@EntityListeners({UserEventListener.class})
public class User implements Auditable, SoftDeletable, Validatable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String email;
private boolean active = true;
// Audit fields
@Column(name = "created_at")
private Instant createdAt;
@Column(name = "created_by")
private String createdBy;
@Column(name = "updated_at")
private Instant updatedAt;
@Column(name = "updated_by")
private String updatedBy;
// Soft delete
private boolean deleted = false;
// Tenant isolation
@Column(name = "tenant_id")
private String tenantId;
// Constructors, getters, setters
public User() {}
public User(String username, String email, String tenantId) {
this.username = username;
this.email = email;
this.tenantId = tenantId;
}
@Override
public Set<ConstraintViolation<Validatable>> validate() {
Set<ConstraintViolation<Validatable>> violations = new HashSet<>();
// Custom validation logic
if (username != null && username.length() < 3) {
// Add constraint violation
}
return violations;
}
// Getters and setters...
}
@Entity
@Table(name = "audit_trail")
public class AuditTrail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "entity_type")
private String entityType;
@Column(name = "entity_id")
private String entityId;
@Enumerated(EnumType.STRING)
private AuditEventType eventType;
@Column(name = "event_timestamp")
private Instant eventTimestamp;
private String user;
@Column(columnDefinition = "JSONB")
private String changes; // JSON representation of changes
@Column(name = "ip_address")
private String ipAddress;
// Getters and setters...
}
enum AuditEventType {
CREATE, UPDATE, DELETE, READ
}
// Custom entity listener
public class UserEventListener {
private static final Logger logger = LoggerFactory.getLogger(UserEventListener.class);
@PrePersist
public void prePersist(User user) {
logger.info("About to persist user: {}", user.getUsername());
if (user.getCreatedAt() == null) {
user.setCreatedAt(Instant.now());
}
}
@PostPersist
public void postPersist(User user) {
logger.info("Successfully persisted user: {}", user.getUsername());
}
@PreUpdate
public void preUpdate(User user) {
logger.info("About to update user: {}", user.getUsername());
user.setUpdatedAt(Instant.now());
}
@PostUpdate
public void postUpdate(User user) {
logger.info("Successfully updated user: {}", user.getUsername());
}
@PreRemove
public void preRemove(User user) {
logger.info("About to remove user: {}", user.getUsername());
}
@PostRemove
public void postRemove(User user) {
logger.info("Successfully removed user: {}", user.getUsername());
}
@PostLoad
public void postLoad(User user) {
logger.debug("Loaded user: {}", user.getUsername());
}
}
Service Layer Integration
Service Classes Using Interceptors/Events
@Service
@Transactional
public class UserService {
private final SessionFactory sessionFactory;
private final AdvancedInterceptor advancedInterceptor;
public UserService(SessionFactory sessionFactory, AdvancedInterceptor advancedInterceptor) {
this.sessionFactory = sessionFactory;
this.advancedInterceptor = advancedInterceptor;
}
public User createUser(User user) {
Session session = sessionFactory.withOptions()
.interceptor(advancedInterceptor)
.openSession();
try {
session.beginTransaction();
session.persist(user);
session.getTransaction().commit();
return user;
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException("Failed to create user", e);
} finally {
session.close();
}
}
public User updateUser(User user) {
Session session = sessionFactory.withOptions()
.interceptor(advancedInterceptor)
.openSession();
try {
session.beginTransaction();
User merged = (User) session.merge(user);
session.getTransaction().commit();
return merged;
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException("Failed to update user", e);
} finally {
session.close();
}
}
public void deleteUser(Long userId) {
Session session = sessionFactory.withOptions()
.interceptor(advancedInterceptor)
.openSession();
try {
session.beginTransaction();
User user = session.get(User.class, userId);
if (user != null) {
session.delete(user);
}
session.getTransaction().commit();
} catch (SoftDeleteException e) {
// Handle soft delete - entity was marked as deleted but not removed
logger.info("User soft-deleted: {}", userId);
session.getTransaction().commit();
} catch (Exception e) {
session.getTransaction().rollback();
throw new RuntimeException("Failed to delete user", e);
} finally {
session.close();
}
}
}
@Service
public class AuditTrailService {
private final AuditTrailRepository auditTrailRepository;
private final ObjectMapper objectMapper;
public AuditTrailService(AuditTrailRepository auditTrailRepository, ObjectMapper objectMapper) {
this.auditTrailRepository = auditTrailRepository;
this.objectMapper = objectMapper;
}
public void recordEvent(Object entity, AuditEventType eventType, String user) {
recordEvent(entity, eventType, user, null);
}
public void recordEvent(Object entity, AuditEventType eventType, String user, Map<String, Object> changes) {
AuditTrail audit = new AuditTrail();
audit.setEntityType(entity.getClass().getSimpleName());
audit.setEntityId(getEntityId(entity));
audit.setEventType(eventType);
audit.setEventTimestamp(Instant.now());
audit.setUser(user);
if (changes != null && !changes.isEmpty()) {
try {
audit.setChanges(objectMapper.writeValueAsString(changes));
} catch (JsonProcessingException e) {
logger.warn("Failed to serialize changes for audit trail", e);
}
}
auditTrailRepository.save(audit);
}
private String getEntityId(Object entity) {
try {
Method getId = entity.getClass().getMethod("getId");
Object id = getId.invoke(entity);
return id != null ? id.toString() : "unknown";
} catch (Exception e) {
return "unknown";
}
}
}
Configuration and Testing
Spring Configuration
@Configuration
@EnableTransactionManagement
public class HibernateConfig {
@Bean
public LocalSessionFactoryBean sessionFactory(DataSource dataSource,
AdvancedInterceptor advancedInterceptor) {
LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setPackagesToScan("com.example.entity");
sessionFactory.setHibernateProperties(hibernateProperties());
sessionFactory.setEntityInterceptor(advancedInterceptor);
return sessionFactory;
}
@Bean
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
HibernateTransactionManager transactionManager = new HibernateTransactionManager();
transactionManager.setSessionFactory(sessionFactory);
return transactionManager;
}
private Properties hibernateProperties() {
Properties properties = new Properties();
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
properties.put("hibernate.show_sql", "true");
properties.put("hibernate.format_sql", "true");
properties.put("hibernate.hbm2ddl.auto", "validate");
properties.put("hibernate.ejb.interceptor", advancedInterceptor());
// Event listeners
properties.put("hibernate.ejb.event.pre-insert", "com.example.listener.AuditEventListener");
properties.put("hibernate.ejb.event.pre-update", "com.example.listener.AuditEventListener");
properties.put("hibernate.ejb.event.pre-delete", "com.example.listener.AuditEventListener");
return properties;
}
@Bean
public AdvancedInterceptor advancedInterceptor() {
return new AdvancedInterceptor();
}
}
Comprehensive Testing
@DataJpaTest
@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"spring.jpa.hibernate.ddl-auto=create-drop"
})
class HibernateInterceptorTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private SessionFactory sessionFactory;
@MockBean
private AuditService auditService;
@MockBean
private SecurityContext securityContext;
@Test
void testInterceptorOnSave() {
// Given
User user = new User("testuser", "[email protected]", "tenant1");
when(securityContext.getCurrentUser()).thenReturn(Optional.of("testuser"));
// When
entityManager.persist(user);
entityManager.flush();
// Then
assertThat(user.getCreatedAt()).isNotNull();
assertThat(user.getCreatedBy()).isEqualTo("testuser");
verify(auditService).logCreation(any(User.class), eq("testuser"));
}
@Test
void testInterceptorOnUpdate() {
// Given
User user = new User("testuser", "[email protected]", "tenant1");
entityManager.persist(user);
entityManager.flush();
when(securityContext.getCurrentUser()).thenReturn(Optional.of("updater"));
// When
user.setEmail("[email protected]");
entityManager.merge(user);
entityManager.flush();
// Then
assertThat(user.getUpdatedAt()).isNotNull();
assertThat(user.getUpdatedBy()).isEqualTo("updater");
verify(auditService).logUpdate(any(User.class), anyMap(), eq("updater"));
}
@Test
void testSoftDelete() {
// Given
User user = new User("testuser", "[email protected]", "tenant1");
user.setSoftDeletable(true);
entityManager.persist(user);
entityManager.flush();
// When/Then
assertThatThrownBy(() -> {
entityManager.remove(user);
entityManager.flush();
}).isInstanceOf(SoftDeleteException.class);
// Verify entity was not actually deleted
User found = entityManager.find(User.class, user.getId());
assertThat(found).isNotNull();
assertThat(found.isDeleted()).isTrue();
}
@Test
void testValidationInterceptor() {
// Given
User user = new User("ab", "invalid-email", "tenant1"); // Invalid data
// When/Then
assertThatThrownBy(() -> {
entityManager.persist(user);
entityManager.flush();
}).isInstanceOf(ConstraintViolationException.class);
}
}
// Manual session testing
@Test
void testCustomInterceptorWithSession() {
CustomInterceptor interceptor = new CustomInterceptor();
Session session = sessionFactory.withOptions()
.interceptor(interceptor)
.openSession();
Transaction transaction = null;
try {
transaction = session.beginTransaction();
User user = new User("sessionuser", "[email protected]", "tenant1");
session.save(user);
transaction.commit();
// Verify interceptor was called
// Add assertions based on interceptor behavior
} finally {
if (transaction != null && transaction.isActive()) {
transaction.rollback();
}
session.close();
}
}
Best Practices and Patterns
Performance Considerations
@Component
public class PerformanceOptimizedInterceptor extends EmptyInterceptor {
private final ThreadLocal<Set<Object>> processedEntities = ThreadLocal.withInitial(HashSet::new);
@Override
public boolean onSave(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
// Avoid processing the same entity multiple times in the same session
if (processedEntities.get().contains(entity)) {
return false;
}
processedEntities.get().add(entity);
// Performance optimization: only process entities that need auditing
if (!(entity instanceof Auditable)) {
return false;
}
// Use bulk operations for better performance
return applyAuditFields(entity, state, propertyNames, "CREATE");
}
@Override
public void afterTransactionCompletion(Transaction tx) {
// Clean up thread local
processedEntities.remove();
}
private boolean applyAuditFields(Object entity, Object[] state, String[] propertyNames, String operation) {
boolean modified = false;
// ... implementation
return modified;
}
}
Security Considerations
@Component
public class SecurityInterceptor extends EmptyInterceptor {
private final SecurityService securityService;
public SecurityInterceptor(SecurityService securityService) {
this.securityService = securityService;
}
@Override
public boolean onLoad(Object entity, Serializable id, Object[] state,
String[] propertyNames, Type[] types) {
// Check if user has permission to access this entity
if (entity instanceof SecuredEntity securedEntity) {
if (!securityService.canRead(securedEntity)) {
throw new SecurityException("Access denied to entity: " + entity.getClass().getSimpleName());
}
}
// Apply data masking for sensitive fields
if (entity instanceof ContainsSensitiveData sensitiveEntity) {
maskSensitiveData(state, propertyNames);
}
return false;
}
private void maskSensitiveData(Object[] state, String[] propertyNames) {
for (int i = 0; i < propertyNames.length; i++) {
if (isSensitiveField(propertyNames[i])) {
state[i] = "***MASKED***";
}
}
}
private boolean isSensitiveField(String fieldName) {
return fieldName.equals("password") ||
fieldName.equals("ssn") ||
fieldName.equals("creditCardNumber");
}
}
Conclusion
Hibernate Interceptors and Events provide powerful capabilities for:
- Audit trails and change tracking
- Data validation and integrity enforcement
- Security and access control
- Business logic enforcement
- Performance monitoring and caching
- Multi-tenancy and data isolation
Key benefits:
- Non-invasive - Business logic remains separate from entities
- Consistent - Applied automatically across all database operations
- Flexible - Can be enabled/disabled per session or globally
- Powerful - Access to Hibernate's internal state and operations
Best practices:
- Keep interceptors focused on single responsibilities
- Be cautious of performance impacts in high-volume scenarios
- Ensure thread safety when using ThreadLocal storage
- Test thoroughly as interceptors affect all database operations
- Use events for read-only operations and interceptors for state modification
By leveraging these powerful Hibernate features, you can implement complex cross-cutting concerns while maintaining clean, maintainable code architecture.