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

ric*_*din 8 spring java-8 spring-boot 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

该方法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();
    }
}
Run Code Online (Sandbox Code Playgroud)

问题是,在向控制器发出未知用户请求的情况下,测试不会返回HTTP 500状态而不是404状态.

这是怎么回事?

ric*_*din 11

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

正如javadoc中所述CompletableFuture

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

在我的情况下,该thenApply方法创建一个新的CompletionStage包装与CompletionException原始实例UserNotFoundException:(

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

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

部分解决方案是将a重新映射CompletableFuture<T>为a 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

我无法理解为什么Spring原生支持CompletableFuture作为控制器的返回值,但它在控制器通知类中无法正确处理.

希望能帮助到你.

  • 您是否向Spring提出了问题?如果ControllerAdvice解开CompletionExceptions并处理原因,那会更干净。 (3认同)