Hibernate Envers 5.x迁6时AuditReader#find抛NPE问题求助
Hibernate Envers 6.x迁移:自定义RevisionListener调用AuditReader#find时的NullPointerException问题
问题场景
从Hibernate Envers 5.x迁移至6.x版本后,项目中存在多对多关联场景(通过带额外字段的中间实体DocumentAuthorEmployee实现,两侧均为@ManyToOne关联),所有实体均标记@Audited(withModifiedFlag = true)。
实现自定义RevisionListener用于对比实体的前后版本差异时,在监听器中调用AuditReader#find方法触发以下NullPointerException:
java.lang.NullPointerException: Cannot invoke "org.hibernate.envers.internal.entities.EntityConfiguration.getRelationDescription(String)" because "entCfg" is null at org.hibernate.envers.internal.entities.EntitiesConfigurations.getRelationDescription(EntitiesConfigurations.java:101) at org.hibernate.envers.internal.entities.mapper.relation.ToOneIdMapper.isIgnoreNotFound(ToOneIdMapper.java:226) at org.hibernate.envers.internal.entities.mapper.relation.ToOneIdMapper.nullSafeMapToEntityFromMap(ToOneIdMapper.java:171) ... at org.hibernate.envers.internal.reader.AuditReaderImpl.find(AuditReaderImpl.java:89) at com.github.csaadaam.envers.manytoone.npe.listener.RevisionListener.entityChanged(RevisionListener.java:20)
原因分析
该问题源于Hibernate Envers 6.0对ToOneIdMapper类的逻辑变更:
- 5.x版本中,代码会先检查关联实体是否被审计(
if ( !referencedEntity.isAudited())),若已审计则跳过相关逻辑。 - 6.x版本中,会尝试从版本数据的map中获取
$type$字段来确定referencingEntityName,当该字段不存在时会导致referencingEntityName为null。后续在EntitiesConfigurations#getRelationDescription方法中,无法获取对应的EntityConfiguration,最终抛出NullPointerException。
相关核心代码片段:
// EntitiesConfigurations.java final EntityConfiguration entCfg; if ( isVersioned( entityName ) ) { entCfg = get( entityName ); } else { entCfg = getNotVersionEntityConfiguration( entityName ); } // 当entityName为null时,entCfg为null,后续调用getRelationDescription会抛NPE
解决方案
1. 延迟差异计算到事务提交后
RevisionListener的entityChanged方法执行时机是在事务提交前,此时Envers的审计数据尚未完全持久化,内部配置也未就绪。建议将版本对比逻辑延迟到事务提交后执行:
- 若使用Spring框架,可通过
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)监听事务完成事件,在事件处理方法中调用AuditReader获取历史版本。 - 原生JPA环境可使用
TransactionSynchronizationRegistry注册同步回调。
2. 显式标记关联字段的审计属性
在中间实体的@ManyToOne字段上显式添加@Audited注解,确保Envers正确识别关联实体的审计配置:
@Audited(withModifiedFlag = true) @Entity public class DocumentAuthorEmployee { // ...其他字段 @ManyToOne @JoinColumn(name = "document_id") @Audited private Document document; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "employee_id") @Audited private Employee employee; // ...getter/setter }
3. 使用AuditQuery替代AuditReader#find
改用Envers的查询API获取历史版本,避免find方法的内部逻辑问题:
// 在合适的时机(事务提交后)执行 AuditReader auditReader = AuditReaderFactory.get(entityManager); AuditQuery query = auditReader.createQuery() .forEntitiesAtRevision(Document.class, currentRevision - 1) .add(AuditEntity.id().eq(documentId)); Document previousVersion = (Document) query.getSingleResult();
4. 升级至最新Envers版本
检查Hibernate Envers的官方issue库,确认该问题是否已被修复。尝试升级到最新的6.x稳定版本(如6.4.x),新版本可能已修复该类内部配置引用的问题。
内容的提问来源于stack exchange,提问作者csaadaam
相关产品推荐
相关产品推荐

