如何让Hibernate Envers仅忽略特定一次数据库操作的审计(避免空指针异常)
我完全理解你的困境:在登录流程中,还没完成Authentication对象的设置就需要保存用户实体,这时候审计逻辑触发会因为SecurityContext里没有Authentication而抛出NPE,而且你不想全局禁用审计,只想跳过这一次操作的审计记录。
这里有几个针对性的解决方案,你可以根据自己的场景选择:
方案一:临时禁用当前会话的Envers审计(推荐)
Hibernate Envers提供了会话级别的参数,可以临时跳过审计操作。你可以在MyUserService里新增一个带开关的保存方法,在需要跳过审计的地方调用它,操作完成后再恢复原来的会话配置,避免影响其他操作。
修改后的MyUserService代码如下:
@Service @RequiredArgsConstructor public class MyUserService { private final EntityManager entityManager; private final MyUserRepository userRepository; // 普通保存方法,正常触发审计 public MyUser saveUser(MyUser user) { return saveUser(user, false); } // 带跳过审计开关的保存方法 public MyUser saveUser(MyUser user, boolean skipAudit) { if (skipAudit) { Session session = entityManager.unwrap(Session.class); // 保存会话原本的审计配置,后续恢复 Object originalAuditFlag = session.getProperty("org.hibernate.envers.skip_audit"); try { // 设置临时跳过审计 session.setProperty("org.hibernate.envers.skip_audit", true); return userRepository.save(user); } finally { // 恢复会话原来的审计状态 if (originalAuditFlag != null) { session.setProperty("org.hibernate.envers.skip_audit", originalAuditFlag); } else { session.clearProperty("org.hibernate.envers.skip_audit"); } } } else { return userRepository.save(user); } } // 你的getCurrentUserId方法保持不变,可加空判断优化 public Optional<Long> getCurrentUserId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null) { return Optional.empty(); } Object principal = authentication.getPrincipal(); if (principal instanceof OAuth2AuthenticatedPrincipal) { // 你的原有逻辑 } return Optional.empty(); } }
然后在你的授权Bean方法里,调用带开关的保存方法:
public Set<String> retrieveUserAuthorities(String username) { Optional<MyUser> userOptional = userRepository.findByUsername(username); if (userOptional.isPresent()) { MyUser user = userOptional.get(); // 你的属性设置逻辑 myUserService.saveUser(user, true); // 传入true跳过这次审计 return ...; } // 其他逻辑 }
这个方案的优点是精准控制单次操作,不会影响其他业务的审计,而且不需要修改实体类的注解配置,非常灵活。
方案二:优化AuditorAware避免NPE(适合允许审计记录为空的场景)
如果你可以接受这次操作生成的审计记录中,审计用户字段为null,那可以直接修改getCurrentUserId方法,提前判断Authentication是否为空,避免抛出NPE:
public Optional<Long> getCurrentUserId() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 先判断Authentication是否为空,避免NPE if (authentication == null || authentication.getPrincipal() == null) { return Optional.empty(); } Object principal = authentication.getPrincipal(); if (principal instanceof OAuth2AuthenticatedPrincipal) { // 你的原有逻辑 } return Optional.empty(); }
这个方案更简单,但会生成一条审计记录,只是审计用户是空的。如果你的业务允许这种情况,这是最快的解决方式。
方案三:临时替换AuditorAware的实现(适合复杂场景)
如果上面的方案都不满足,你还可以利用Spring的ApplicationContext,在需要跳过审计的操作期间,临时替换AuditorAware的bean为一个返回空的实现,操作完成后再恢复。不过这种方式相对复杂,需要处理线程安全,一般不推荐,除非有特殊需求。
示例代码如下:
// 在你的授权Bean里注入ApplicationContext private final ApplicationContext context; public Set<String> retrieveUserAuthorities(String username) { Optional<MyUser> userOptional = userRepository.findByUsername(username); if (userOptional.isPresent()) { MyUser user = userOptional.get(); // 保存原来的AuditorAware bean AuditorAware<Long> originalAuditor = context.getBean(AuditorAware.class); // 注册临时的AuditorAware,返回空 context.getBeanFactory().registerSingleton("auditorAware", (AuditorAware<Long>) Optional::empty); try { myUserService.saveUser(user); } finally { // 恢复原来的AuditorAware bean context.getBeanFactory().registerSingleton("auditorAware", originalAuditor); } return ...; } // 其他逻辑 }
不过这个方案需要注意线程安全,因为Spring的bean默认是单例的,多线程环境下可能有问题,所以一般优先选择方案一。
备注:内容来源于stack exchange,提问作者Andrei Manolache

