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



我正在使用 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在后续阶段如何处理异常。

如 javadocCompletableFuture中所述

[..] 如果一个阶段的计算突然终止,并出现(未选中的)异常或错误,则所有需要完成它的依赖阶段也会异常完成,CompletionException 将异常作为其原因。[..]

就我而言,thenApply方法创建了一个新的CompletionStage实例,该实例用原始CompletionException包装UserNotFoundException:(

遗憾的是,控制器建议不执行任何解包操作。Zalando开发人员也发现了这个问题:Async CompletableFuture追加错误

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

部分解决方案是将CompletableFuture<T>重新映射到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;
}
}

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

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

我不明白为什么 Spring 本身支持CompletableFuture作为控制器的返回值,但它在控制器建议类中无法正确处理。

希望对您有所帮助。

对于那些仍然遇到麻烦的人:即使 Spring 正确地解包了 ExecutionException,如果你有一个类型"Exception"的处理程序,它不起作用,它被选择来处理 ExecutionException,而不是根本原因的处理程序。

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

相关内容

  • 没有找到相关文章

最新更新