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

如何让Hibernate Envers仅忽略特定一次数据库操作的审计(避免空指针异常)

如何让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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 09:33:12