Spring Data JPA:@PostUpdate后写入审计表的异常解决
JPA实体审计监听器异常问题解决
问题背景
存在两个JPA实体Bo3和Bo3Audit,需求为:当Bo3的指定审计字段(prop1、prop2)插入非空值或发生更新时,向Bo3Audit表插入对应记录。当前@PrePersist监听器工作正常,但@PostUpdate监听器抛出ConcurrentModificationException;若改用@PreUpdate则出现StackOverflowError。手动在业务逻辑中调用postUpdate方法可正常运行,需解决监听器的异常问题。
实体与仓库代码
Bo3实体
@Entity // 其他JPA注解 public class Bo3 { // 持久化字段 prop1, prop2, prop3 // prop1、prop2为审计字段 @Transient @JsonIgnore private List<String> myAuditFields = List.of("prop1", "prop2"); // 获取审计字段对应的getter方法流的Supplier @Transient @JsonIgnore Supplier<Stream<Method>> streamSupplier = () -> Arrays.stream(Bo3Audit.class.getDeclaredFields()) .filter(f -> myAuditFields.contains(f.getName())) .map(f -> Objects.requireNonNull( BeanUtils.findMethod(Bo3Audit.class, "get" + StringUtils.capitalize(f.getName())) )); }
Bo3Audit实体
@Entity // 其他JPA注解 public class Bo3Audit { // 持久化字段 prop1, prop2(仅包含Bo3的审计字段) @ManyToOne(fetch = FetchType.LAZY) private Bo3 bo3; }
Bo3Audit仓库
@Repository public interface Bo3AuditRepository extends JpaRepository<Bo3Audit, Long> { @Query("from Bo3Audit where bo3.id = :id order by createdAt desc") List<Bo3Audit> findLastAudit(Long id, Pageable pageable); }
监听器代码
正常工作的@PrePersist监听器
@PrePersist public void prePersist() { Bo3Audit bo3Audit = generateAudit(this); // 复制prop1、prop2并将当前Bo3实例关联到bo3字段 streamSupplier.get().map(m -> { try { return m.invoke(bo3Audit); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } }) .filter(Objects::nonNull) .findAny().ifPresent(value -> { MyBeanUtils.getBean(Bo3AuditRepository.class).save(bo3Audit); }); }
抛出异常的@PostUpdate监听器
@PostUpdate public void postUpdate() { Bo3AuditRepository repo = MyBeanUtils.getBean(Bo3AuditRepository.class); Bo3Audit bo3Audit = generateAudit(this); // 复制prop1、prop2并将当前Bo3实例关联到bo3字段 List<Bo3Audit> lastAudits = repo.findLastAudit(this.getId(), PageRequest.of(0,1)); /// // 此处为使用streamSupplier检查prop1、prop2是否有变化的逻辑 /// if (isChanged) { repo.save(bo3Audit); } }
异常情况
- @PostUpdate异常:抛出
ConcurrentModificationException,堆栈信息如下:
java.util.ConcurrentModificationException: null at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967) at java.base/java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1054) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:602) at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478) at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475) at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344) at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40) at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:489) at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3303) at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2438) at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:449) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40) at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281) at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101) at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562) at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:743) at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:711) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:654) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:407) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) at com.paanini.generated.app.jiffy.controller.defaultInternalServiceController$$EnhancerBySpringCGLIB$$cff4506b.UpdateBo3(<generated>)
- @PreUpdate异常:在
repo.findLastAudit行附近抛出StackOverflowError - 手动调用正常:移除
@PostUpdate注解,在bo3Repository.save(newBo3)后手动调用postUpdate方法可正常运行
原因分析
- @PostUpdate的ConcurrentModificationException:JPA的
@PostUpdate事件发生在实体更新操作已加入Session的ActionQueue,但事务尚未提交的阶段。此时调用repo.save会触发新的持久化操作,修改ActionQueue的集合结构,而Hibernate在遍历ActionQueue执行操作时不允许并发修改,因此抛出异常。 - @PreUpdate的StackOverflowError:
@PreUpdate阶段处于实体更新的生命周期中,调用repo.findLastAudit会触发Session flush操作,进而再次触发@PreUpdate事件,形成无限递归,最终导致栈溢出。
解决方案
方案一:使用Hibernate的@PostCommitUpdate事件
该事件在事务提交后触发,此时Session的ActionQueue已处理完毕,不会出现并发修改问题。
import org.hibernate.annotations.PostCommitUpdate; @Entity public class Bo3 { // ... 其他字段及方法 @PostCommitUpdate public void postCommitUpdate() { Bo3AuditRepository repo = MyBeanUtils.getBean(Bo3AuditRepository.class); Bo3Audit bo3Audit = generateAudit(this); List<Bo3Audit> lastAudits = repo.findLastAudit(this.getId(), PageRequest.of(0,1)); // 实现字段变化检查逻辑 boolean isChanged = false; if (!lastAudits.isEmpty()) { Bo3Audit lastAudit = lastAudits.get(0); // 对比prop1、prop2是否变化 isChanged = !Objects.equals(lastAudit.getProp1(), bo3Audit.getProp1()) || !Objects.equals(lastAudit.getProp2(), bo3Audit.getProp2()); } else { // 无历史审计记录,若当前字段非空则视为变化 isChanged = bo3Audit.getProp1() != null || bo3Audit.getProp2() != null; } if (isChanged) { repo.save(bo3Audit); } } }
方案二:Spring事件驱动异步处理
将审计操作从JPA生命周期事件中剥离,通过Spring事件异步处理,避免干扰当前Session的操作流程。
1. 定义事件类
public class Bo3UpdatedEvent { private final Bo3 bo3; public Bo3UpdatedEvent(Bo3 bo3) { this.bo3 = bo3; } public Bo3 getBo3() { return bo3; } }
2. 在Bo3的@PostUpdate中发布事件
import org.springframework.context.ApplicationEventPublisher; import org.springframework.beans.factory.annotation.Autowired; @Entity public class Bo3 { @Transient @Autowired private ApplicationEventPublisher eventPublisher; // ... 其他字段及方法 @PostUpdate public void postUpdate() { eventPublisher.publishEvent(new Bo3UpdatedEvent(this)); } }
3. 实现事件监听器
import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component public class Bo3AuditListener { private final Bo3AuditRepository bo3AuditRepository; public Bo3AuditListener(Bo3AuditRepository bo3AuditRepository) { this.bo3AuditRepository = bo3AuditRepository; } @EventListener @Transactional public void handleBo3Updated(Bo3UpdatedEvent event) { Bo3 bo3 = event.getBo3(); Bo3Audit bo3Audit = generateAudit(bo3); List<Bo3Audit> lastAudits = bo3AuditRepository.findLastAudit(bo3.getId(), PageRequest.of(0,1)); // 字段变化检查逻辑 boolean isChanged = false; if (!lastAudits.isEmpty()) { Bo3Audit lastAudit = lastAudits.get(0); isChanged = !Objects.equals(lastAudit.getProp1(), bo3Audit.getProp1()) || !Objects.equals(lastAudit.getProp2(), bo3Audit.getProp2()); } else { isChanged = bo3Audit.getProp1() != null || bo3Audit.getProp2() != null; } if (isChanged) { bo3AuditRepository.save(bo3Audit); } } private Bo3Audit generateAudit(Bo3 bo3) { Bo3Audit audit = new Bo3Audit(); audit.setProp1(bo3.getProp1()); audit.setProp2(bo3.getProp2()); audit.setBo3(bo3); return audit; } }
优化建议
缓存审计字段的getter方法,避免每次反射调用的性能损耗:
@Transient @JsonIgnore private static final List<Method> AUDIT_GETTERS; static { AUDIT_GETTERS = Arrays.stream(Bo3Audit.class.getDeclaredFields()) .filter(f -> List.of("prop1", "prop2").contains(f.getName())) .map(f -> { try { return Bo3Audit.class.getMethod("get" + StringUtils.capitalize(f.getName())); } catch (NoSuchMethodException e) { throw new RuntimeException("找不到审计字段的getter方法", e); } }) .collect(Collectors.toList()); } // 使用时直接调用AUDIT_GETTERS.stream()
内容的提问来源于stack exchange,提问作者Ramanujan R
相关产品推荐
相关产品推荐

