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 将充当一个包罗万象的角色。
我正在使用 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 将充当一个包罗万象的角色。