Spring控制器建议无法正确处理CompletableFuture异常完成

时间:2018-04-05 16:08:36

标签: spring spring-boot java-8 completable-future

我正在使用Spring Boot 1.5,我有一个异步执行的控制器,返回CompletableFuture<User>

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private final UserService service;

    @GetMapping("/{id}/address")
    public CompletableFuture<Address> getAddress(@PathVariable String id) {
        return service.findById(id).thenApply(User::getAddress);
    }
}

方法UserService.findById可以抛出UserNotFoundException。所以,我开发了专门的控制器建议。

@ControllerAdvice(assignableTypes = UserController .class)
public class UserExceptionAdvice {
    @ExceptionHandler(UserNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public String handleUserNotFoundException(UserNotFoundException ex) {
        return ex.getMessage();
    }
}

问题是,如果向控制器发出未知用户请求,测试不会返回HTTP 500状态而不是404状态。

发生了什么事?

2 个答案:

答案 0 :(得分:9)

问题是由于完成的异常CompletableFuture如何在后续阶段处理异常。

CompletableFuture javadoc

中所述
  

[..]如果一个阶段的计算突然以(未经检查的)异常或错误终止,那么所有需要完成的依赖阶段也会异常完成,并且CompletionException将异常作为其原因。 [..]

就我而言,thenApply方法会创建一个CompletionStage的新实例,其中包含原始CompletionException的{​​{1}} :(

可悲的是,控制器建议不执行任何解包操作。 Zalando开发人员也发现了这个问题:Async CompletableFuture append errors

因此,使用UserNotFoundException和控制器建议在Spring中实现异步控制器似乎不是一个好主意。

部分解决方案是将CompletableFuture重新映射为CompletableFuture<T>In this blog,给出了可能的适配器的实现。

DeferredResult<T>

因此,我的原始控制器将更改为以下内容。

public class DeferredResults {

    private DeferredResults() {}

    public static <T> DeferredResult<T> from(final CompletableFuture<T> future) {
        final DeferredResult<T> deferred = new DeferredResult<>();
        future.thenAccept(deferred::setResult);
        future.exceptionally(ex -> {
            if (ex instanceof CompletionException) {
                deferred.setErrorResult(ex.getCause());
            } else {
                deferred.setErrorResult(ex);
            }
            return null;
        });
        return deferred;
    }
}

我无法理解为什么Spring本身支持@GetMapping("/{id}/address") public DeferredResult<Address> getAddress(@PathVariable String id) { return DeferredResults.from(service.findById(id).thenApply(User::getAddress)); } 作为控制器的返回值,但它在控制器通知类中无法正确处理。

希望它有所帮助。

答案 1 :(得分:1)

对于那些仍然遇到这个问题的人:即使 Spring 正确解包了 ExecutionException,如果您有一个“Exception”类型的处理程序,它被选择来处理 ExecutionException,而不是根本原因的处理程序。

解决方案:使用“异常”处理程序创建第二个 ControllerAdvice,并将 @Order(Ordered.HIGHEST_PRECEDENCE) 放在常规处理程序上。这样,您的常规处理程序将首先运行,而您的第二个 ControllerAdvice 将充当全部。