我正在使用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状态。
发生了什么事?
答案 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 将充当全部。