使用Hibernate Interceptor做数据库审计时删除操作被回滚的问题求助
根因分析
- 静态字段注入失效:你给static修饰的
auditRepo加@Autowired,Spring默认不支持静态字段注入,导致auditRepo为null,调用save时报空指针,但你仅捕获了HibernateException,异常被向上抛出触发了当前业务事务回滚,所以审计日志和删除操作都被回滚,且看不到错误日志。 - 审计日志与业务操作同事务:审计日志保存和业务删除操作在同一个事务中,若业务事务后续回滚,审计日志也会被一同回滚;同时删除后的实体已经被session标记为删除,后续在postFlush中读取实体属性可能失败。
- 拦截器线程安全问题:
inserts/updates/deletes是普通成员变量,Hibernate拦截器默认是单例,多线程并发时会互相覆盖数据,导致日志丢失。 - 异常被吞:仅捕获
HibernateException,其他运行时异常(如空指针、事务异常)不会被捕获,也无日志输出,无法定位问题。
可行解决方案
1. 修复依赖注入问题
推荐直接去掉auditRepo的static修饰,如果必须保留static可通过setter方式注入:
// 方式1:去掉static修饰(推荐) @Autowired private AuditRepo auditRepo; // 方式2:保留static的适配方案 private static AuditRepo auditRepo; @Autowired public void setAuditRepo(AuditRepo auditRepo) { AuditLogInterceptor.auditRepo = auditRepo; }
2. 修复异常捕获逻辑
扩大异常捕获范围,增加错误日志输出,避免吞异常:
private void logIt(String action, Object entity) { try { AuditLog auditLog = new AuditLog(); // 原有赋值逻辑不变 auditRepo.save(auditLog); } catch (Exception e) { // 换成项目实际使用的日志组件打印错误即可 System.err.println("保存审计日志失败: " + e.getMessage()); e.printStackTrace(); // 如果不需要因为审计失败导致业务操作回滚,就不要抛出异常 } }
3. 单独事务存储审计日志
给审计保存方法配置REQUIRES_NEW事务传播级别,新开独立事务存储审计,不受业务事务回滚影响:
// 注解加在logIt方法上,或者AuditRepo的save方法上均可 @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) private void logIt(String action, Object entity) { // 原有逻辑不变 }
4. 提前采集删除操作的属性
不要在onDelete中存储实体对象,提前把需要的审计字段采集出来,避免后续实体被删除后无法读取属性:
// 调整deletes集合的存储结构,用ThreadLocal保证线程安全的同时存储预采集的审计数据 private ThreadLocal<Set<Map<String, Object>>> deletes = ThreadLocal.withInitial(HashSet::new); @Override public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { if (entity instanceof IAuditLog) { IAuditLog auditEntity = (IAuditLog) entity; Map<String, Object> deleteRecord = new HashMap<>(); deleteRecord.put("entityId", auditEntity.getId()); deleteRecord.put("entityName", entity.getClass().getSimpleName()); // 其他需要的字段如操作人、操作时间可以提前在这里赋值 deletes.get().add(deleteRecord); } } // postFlush中处理删除日志时直接读取预存的属性即可 for (Map<String, Object> record : deletes.get()) { AuditLog auditLog = new AuditLog(); auditLog.setEntityId((Long) record.get("entityId")); auditLog.setEntityName((String) record.get("entityName")); auditLog.setAction("Deleted"); auditRepo.save(auditLog); }
5. 全量修复线程安全问题
所有待处理的审计集合都用ThreadLocal包装,避免多线程数据冲突,使用后调用remove()避免内存泄漏:
private ThreadLocal<Set<Object>> inserts = ThreadLocal.withInitial(HashSet::new); private ThreadLocal<Set<Object>> updates = ThreadLocal.withInitial(HashSet::new); private ThreadLocal<Set<Map<String, Object>>> deletes = ThreadLocal.withInitial(HashSet::new); // 所有操作集合的地方都要调用get()获取当前线程的集合,比如inserts.get().add(entity) // postFlush的finally块清空集合时调用remove } finally { inserts.remove(); updates.remove(); deletes.remove(); }
内容的提问来源于stack exchange,提问作者parkavi
相关产品推荐
相关产品推荐

