You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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方法可正常运行

原因分析

  1. @PostUpdate的ConcurrentModificationException:JPA的@PostUpdate事件发生在实体更新操作已加入Session的ActionQueue,但事务尚未提交的阶段。此时调用repo.save会触发新的持久化操作,修改ActionQueue的集合结构,而Hibernate在遍历ActionQueue执行操作时不允许并发修改,因此抛出异常。
  2. @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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 21:38:10