Hibernate事件监听器操作触发ConcurrentModificationException问题排查
问题
为数据库表配置了Hibernate监听器,触发时想查询另一张表并更新数据,但出现如下错误:
org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:571) ... Caused by: java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
代码示例:
@Slf4j @RequiredArgsConstructor @Component public class MyTableHibernateListener implements PostInsertEventListener, PreUpdateEventListener, PostUpdateEventListener { private final JPQLQueryFactory queryFactory; @Override public boolean onPreUpdate(PreUpdateEvent event) { try { ... // Some working logic to set some data within the current MyTable. // Try to query another table below AnotherTable row = queryFactory.select(QAnotherTable.anotherTable) .from(QAnotherTable.anotherTable) .where(...) .fetchOne(); ... log.info("Success"); return false; } catch (Exception e) { log.error("Failure", e); throw e; } } }
日志已打印“Success”且无失败日志,说明错误出现在监听器方法外部。尚未对另一张表执行修改,仅查询就触发了问题。
原因分析
- 监听器执行时机与Hibernate内部流程冲突:
PreUpdateEventListener在事务提交前、Hibernate维护实体脏数据的关键阶段运行。此时Hibernate正遍历内部集合(比如待更新实体列表),而你用JPQLQueryFactory执行查询时,可能触发Session自动flush操作——这会修改Hibernate正在遍历的内部集合,直接引发ConcurrentModificationException。 - QueryFactory绑定当前Session的副作用:如果
JPQLQueryFactory复用当前事务的Session,查询操作会触发Hibernate自动flush脏数据,干扰监听器执行时的内部状态维护逻辑,导致集合并发修改。
解决办法
方法1:使用独立Session执行查询
避开当前事务的Session,新建独立Session执行跨表查询,避免干扰Hibernate内部流程:
// 注入EntityManagerFactory private final EntityManagerFactory entityManagerFactory; @Override public boolean onPreUpdate(PreUpdateEvent event) { try { // ...原有逻辑 // 打开独立Session,不参与当前事务 try (Session session = entityManagerFactory.unwrap(SessionFactory.class).openSession()) { JPQLQueryFactory independentQueryFactory = new JPQLQueryFactory(session); AnotherTable row = independentQueryFactory.select(QAnotherTable.anotherTable) .from(QAnotherTable.anotherTable) .where(...) .fetchOne(); // 后续操作 } log.info("Success"); return false; } catch (Exception e) { log.error("Failure", e); throw e; } }
方法2:临时禁用自动Flush
在查询前将当前Session的FlushMode设为MANUAL,避免查询触发自动flush,执行完后恢复原有模式:
@Override public boolean onPreUpdate(PreUpdateEvent event) { try { // ...原有逻辑 Session session = event.getSession(); FlushMode originalMode = session.getFlushMode(); try { session.setFlushMode(FlushMode.MANUAL); AnotherTable row = queryFactory.select(QAnotherTable.anotherTable) .from(QAnotherTable.anotherTable) .where(...) .fetchOne(); // 后续操作 } finally { session.setFlushMode(originalMode); } log.info("Success"); return false; } catch (Exception e) { log.error("Failure", e); throw e; } }
方法3:改用PostCommit阶段执行操作
如果业务允许,切换到PostCommitUpdateEventListener,该监听器在事务提交完成后触发,此时Hibernate内部状态维护已结束,不会出现并发修改问题:
@Slf4j @RequiredArgsConstructor @Component public class MyTableHibernateListener implements PostCommitUpdateEventListener { private final JPQLQueryFactory queryFactory; @Override public void onPostUpdateCommitFailed(PostUpdateEvent event) { // 处理提交失败场景 } @Override public void onPostUpdate(PostUpdateEvent event) { try { AnotherTable row = queryFactory.select(QAnotherTable.anotherTable) .from(QAnotherTable.anotherTable) .where(...) .fetchOne(); // 执行更新操作(需开启新事务) log.info("Success"); } catch (Exception e) { log.error("Failure", e); throw e; } } }
注意:PostCommit阶段不在原事务范围内,若需修改数据需手动开启新事务。
内容的提问来源于stack exchange,提问作者Azianese
相关产品推荐
相关产品推荐

