2

在使用JPA审计功能后,项目保存问卷过程中出现了如下错误,根据错误信息可知,是提交jpa事务时发生错误。

2021-03-20 14:52:04.570 ERROR 21225 --- [io-8002-exec-10] c.y.q.e.GlobalExceptionHandler           : 程序运行异常: 主机 127.0.0.1 调用地址 http://localhost/admin/questionnaire 错误信息 Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Error while committing the transaction

当时在保存问卷过程中存在

@CreatedBy
private User createUser;

/**
 * 在保存之前设置创造时间. 
 */
 @PrePersist
public void initCreateTime() {
  this.createTime = new Timestamp(System.currentTimeMillis());
}
/**
 * 在保存之后设置token. 
 */
 @PostPersist
public void initToken() {
...
}

自动设置user,保存前设置创造时间,保存后设置token操作。怀疑是设置user后提交了事务,导致设置token操作提交时报错。进而得出@CreatedBy注解与@PostPersist注解无法一起使用的结论

解决

尝试在save方法上添加事务注解。结果还是报错。
尝试在则需要将 @PrePersist @PostPersist注解的方法移动到实体监听器中,结果还是报错。
将设置token方法放到m层中,监听器调用m层设置token方法,结果监听器无法注入m层。
切面完成token设置,未尝试。
对m层save方法进行集成测试,排除其他的影响,发现能正确执行,排除了@CreatedBy注解与@PostPersist注解的冲突问题,问题出在别处。
将日志等级变更为debug,查看报错原因。
image.png
根据堆栈信息,发现在保存问卷过程中执行了获取当前登录用户的方法,该方法操作了数据库,导致提交了事务,从而使得@CreateBy注解在获取当前登录用户后,再次更新数据时无事务可用。

/**
 * 审计 获取当前登录用户实现. 
 */
 private class SpringSecurityAuditorAware implements AuditorAware<User> {
  @Autowired
 private UserService userService;
 @Override
 public Optional<User> getCurrentAuditor() {
    User user = this.userService.getCurrentLoginUser();
 if (user == null) {
      throw new RuntimeException("未获取到当前登录用户");
 }
    return Optional.of(user);
 }
}

启用审计功能在对数据库执行操作时总是会获取当前登录用户。,我们之前的方法就是从数据库中查询。
问题找到了,剩下的就是使得此方法不操作数据库,最简单的思路就是像前台一样设置缓存。
在用户登录的时候设置user,当要获取当前登录用户时不再进行数据库操作,而是直接返回user。

/**
 * 自定义认证用户,以此将由数据库中查询出的用户传给认证相关方法使用,比如@CreateBy. 
 */
 public static class AuthUser extends org.springframework.security.core.userdetails.User {
  private User user;
 public AuthUser(User user, Collection<? extends GrantedAuthority> authorities) {
    super(user.getUsername(), user.getPassword(), authorities);
 this.user = user;
 }
  public AuthUser(User user,
 boolean enabled,
 boolean accountNonExpired,
 boolean credentialsNonExpired,
 boolean accountNonLocked,
 Collection<? extends GrantedAuthority> authorities) {
    super(user.getUsername(),
 user.getPassword(),
 enabled,
 accountNonExpired,
 credentialsNonExpired,
 accountNonLocked,
 authorities);
 this.user = user;
 }
  public User getUser() {
    return user;
 }
  protected void setUser(User user) {
    this.user = user;
 }
}

自定义AuthUser类

logger.debug("构造用户");
return new AuthUser(
    user,
    true,
    true,
    true,
    user.isNonLocked(),
    authorities);

登录时执行AuthUser类构造函数,赋予user值

UserServiceAuth.AuthUser authUser =
    (UserServiceAuth.AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
user = authUser.getUser();

获取当前登录用户时调用getUser()方法,从而避免操作数据库,对事务进行提交。

总结

解决bug应该熟悉出错的流程,知道到底哪里错了,然后再想解决方案。


小强Zzz
1.2k 声望32 粉丝