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

Spring controller advice does not correctly handle a CompletableFuture completed exceptionally

我正在使用 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 状态,以防未知用户向控制器发出请求。

怎么回事?

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

CompletableFuture javadoc

中所述

[..] if a stage's computation terminates abruptly with an (unchecked) exception or error, then all dependent stages requiring its completion complete exceptionally as well, with a CompletionException holding the exception as its cause. [..]

在我的例子中,thenApply 方法创建了一个 CompletionStage 的新实例,它用 CompletionException 包装了原始的 UserNotFoundException :(

遗憾的是,控制器建议不执行任何展开操作。 Zalando 开发者也发现了这个问题:Async CompletableFuture append errors

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

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

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;
    }
}

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

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

我无法理解为什么 Spring 原生支持 CompletableFuture 作为控制器的 return 值,但它在控制器建议 类.[=26 中无法正确处理=]

希望对您有所帮助。

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

解决方案:使用“异常”处理程序创建第二个 ControllerAdvice,并将 @Order(Ordered.HIGHEST_PRECEDENCE) 放在常规处理程序上。这样,您的常规处理程序将首先执行,您的第二个 ControllerAdvice 将充当一个包罗万象的角色。