4

在重写分配任务算法时需要获取当前登陆用户区域及其子区域,重写之后进行测试的时候发现任务直接停留在了分配中,查看后台日志时发现了报错
图片.png
根据报错查看抛出错误的地方
图片.png
可以发现是在调用getAuthUserDetailWithoutTransaction方法的时候出现的问题,进一步查看这个方法:

public Optional<AuthUserDetails> getAuthUserDetailWithoutTransaction() {
    logger.debug("根据认证获取当前登录用户名,并获取该用户");

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {
      AuthUserDetails userDetail;
      if (authentication instanceof UsernamePasswordAuthenticationToken) {
        userDetail = (AuthUserDetails) authentication.getPrincipal();
      } else if (authentication instanceof AuthUserDetails) {
        userDetail = (AuthUserDetails) authentication;
      } else if (authentication instanceof AnonymousAuthenticationToken) {
        return Optional.empty();
      } else {
        throw new RuntimeException("获取类型不正确");
      }
      return Optional.of(userDetail);
    }

    logger.debug("认证用户在数据库中不存在");
    return Optional.empty();
  }

根据后台日志输出的认证用户在数据库中不存在可以得出authentication为null,然后为了进一步确认我又尝试用debug模式运行后台发现在后台运行过程中经常会用到上面这个函数,并且获取到的authentication都不为null
图片.png
只有在分配任务时返回的authentication为null
图片.png

可以从下面这个简化的图得知函数调用关系:

又因为报错信息中包含

Unexpected exception occurred invoking async method(调用异步方法时发生意外异常)

所以我尝试去掉addTaskDetailsAndAddFormItemValuesAndUpdateStatistics方法的异步注解,重新执行后发现一切正常,没有发生报错。
所以就去查询了一下SecurityContextHolder.getContext()@Async同时使用会发生的问题,发现@Async方法中使用SecurityContextHolder.getContext()就会返回
null。

测试:
图片.png

图片.png

运行结果:
图片.png
即在异步函数内调用就会返还null,并且我们还可以发现我们当前的默认线程为nio-8081-exec-8,但是异步函数会新建一个task-1线程,并且在新线程中获取不到安全上下文,可以推测默认情况下安全上下文只会存在于主线程中,它不会随着线程的新建而被传递过来。

SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。这也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

如果我们在getAuthUserDetailWithoutTransaction中这样设置安全上下文策略便可解决此问题,但是这样的话又可能对调用此方法的其他方法造成影响,所以我们不得不寻找新的解决方法。

 public Optional<AuthUserDetails> getAuthUserDetailWithoutTransaction() {
    logger.debug("根据认证获取当前登录用户名,并获取该用户");
    //设置可继承本地线程策略,使得其可以在异步方法中被调用
    SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

    . . .
}

起初我想的是只需要在这个异步方法中设置策略即可,但是测试后发现此策略并不会随着方法的调用而被传递过来。
于是我尝试直接在M层获取到当前登陆用户再将登陆用户传给异步方法
图片.png
但是再次尝试时又发生了新的报错

failed to lazily initialize a collection of role: club.yunzhi.smartcommunity.entity.District.children, could not initialize proxy - no Session

搜索后发现Hibernate再进行关联处理时通常用懒加载,可以避免大量数据多次传递,也就是说我们在Aservice里获取到了user,user对应的区域的子区域可以在Aservice中调用,但是如果把user又传给Bservice,那么再想调用user对应的区域的子区域就会发生报错。
源service:
图片.png
接受传递的service:
图片.png
也就是说我们在进行传递参数时为了避免这种情况的发生可以直接传递相应的id,再根据ID通过仓库层获取我们的数据。
也就是说我们可以改成如下这种方式达到目的

 public void taskAssign(){
   .   .   .   
 Optional<Long> optionalWebUserId = this.webUserService.getCurrentLoginWebUserId();
 Long webUserId = optionalWebUserId.orElse(null);
 // 异步生成任务详情与表单,并更新各区域统计情况。
 this.taskDetailAsyncService.addTaskDetailsAndAddFormItemValuesAndUpdateStatistics(task, residents, specification, webUserId);
}
List<District> getManageDistrictsWithCurrentLoginUser(Long ...webUserId);
  public final List<District> getManageDistrictsWithCurrentLoginUser(Long ...webUserId) {
    List<District> result;
    Optional<WebUser> optionalUser;

    if(webUserId.length == 0) {
      optionalUser =  this.webUserService.getCurrentLoginWebUser();
    } else {
      optionalUser = this.webUserRepository.findById(webUserId[0]);
   }
    . . .

    }

此时再次尝试便可达到我们想要的效果。

另外再说一下如何展示多选或单选组件,我们想要达到的效果:
图片.png
并且用户点击选项2也不会改变显示,即类似于disable的效果,但是如果我们直接声明input为disable就会像下面这样显示得不是很清晰。
图片.png
如果我们常使用redonly的话会发现点击后还是会给予相应的显示只是不会改变value值。
解决方法:input中声明onclick为return false

<input
       .  .  .
       onclick = "return false"
       type="radio" >

问题三:
项目中如果一个人有多处房产,那么分配任务后再进行预览就会发现多个记录
图片.png
遇到这种情况我首先想到的就是分配任务时生成数据生成重复了,但是看完分配代码并没有发现任何问题,在数据库中查看也没有生成重复的数据项。也确认了前调用的端口与后台相匹配,于是就尝试在前台输出一下page,结果发现返回来的数据一模一样,同样也在后台查看了返还给前台的数据发现时返还了三个相同的数据,对应的id也是相同的。
图片.png
到这里就本就可以确认是在查询上出了问题。
但是我们在前台没有传递任何查询参数,所以基本可以确认是后台在根据用户区域进行查询时出了问题。
后台查询条件是这样构造的

Join<TaskDetail, Building> buildingJoin = root.join("resident")
          .join("houses", JoinType.LEFT)
          .join("building", JoinType.LEFT);
switch (district.getType()) {
case TYPE_BUILDING:
          return criteriaBuilder.equal(buildingJoin.get("id").as(Long.class), districtId);
 . . .
}

正确的
也就是说每个查询都是独立的,好呢局house1查询完返还一个数据,再根据其他house查询再进行返还,并不是我想象中的下图的方式:
错误的
如果我们想要去除这些重复项只需在查询条件里新增这样一条即可:

 public static Specification<TaskDetail> distinct() {
    return (root, criteriaQuery, criteriaBuilder) -> {
      criteriaQuery.distinct(true);
      return criteriaQuery.getRestriction();
    };
  }

李明
441 声望18 粉丝