You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring集成Axon时抛异常仍返回HTTP 200状态码问题排查

问题分析

核心原因是Axon的CommandGateway会把命令执行的异常封装到CompletableFuture的异常结果中,而Spring Web直接返回CompletableFuture时,会默认认为Future已完成(哪怕是异常完成),因此返回200状态码。同时FailureLoggingCallback仅负责记录异常,不会把异常重新抛入Spring的异常处理链路,导致@ControllerAdvice无法捕获到异常。

解决方案

1. 显式处理CompletableFuture异常,手动抛出异常

不要直接返回CommandGateway.send()生成的CompletableFuture,而是通过处理Future的异常逻辑,把封装的异常重新抛出,让Spring的异常处理器能捕获到:

@PostMapping("/memberships")
public ResponseEntity<Void> createMembership(@RequestBody CreateMembershipCommand command) {
    commandGateway.send(command)
        .whenComplete((result, ex) -> {
            if (ex != null) {
                // 解包Axon包装的CommandExecutionException,拿到真实领域异常
                Throwable actualException = ex instanceof CommandExecutionException 
                    ? ((CommandExecutionException) ex).getCause() 
                    : ex;
                if (actualException instanceof DomainException) {
                    throw (DomainException) actualException;
                }
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
            }
        })
        .join(); // 阻塞等待结果,将异常抛入Spring处理链路
    return ResponseEntity.ok().build();
}

如果用sendAndWait(),要注意解包Axon抛出的CommandExecutionException:

@PostMapping("/memberships")
public ResponseEntity<Void> createMembership(@RequestBody CreateMembershipCommand command) {
    try {
        commandGateway.sendAndWait(command);
    } catch (CommandExecutionException ex) {
        Throwable cause = ex.getCause();
        if (cause instanceof DomainException) {
            throw (DomainException) cause;
        }
        throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
    }
    return ResponseEntity.ok().build();
}

2. 调整@ControllerAdvice的异常捕获范围

让全局异常处理器能直接捕获CommandExecutionException,并解包其中的领域异常:

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(CommandExecutionException.class)
    public ResponseEntity<ErrorResponse> handleCommandExecutionException(CommandExecutionException ex) {
        Throwable cause = ex.getCause();
        if (cause instanceof DomainException) {
            DomainException domainEx = (DomainException) cause;
            return ResponseEntity.status(domainEx.getStatus())
                .body(new ErrorResponse(domainEx.getErrorCode(), domainEx.getMessage()));
        }
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(new ErrorResponse("INTERNAL_ERROR", ex.getMessage()));
    }

    @ExceptionHandler(DomainException.class)
    public ResponseEntity<ErrorResponse> handleDomainException(DomainException ex) {
        return ResponseEntity.status(ex.getStatus())
            .body(new ErrorResponse(ex.getErrorCode(), ex.getMessage()));
    }
}

3. 检查命令总线与回调配置

  • 确认SimpleCommandBus没有被自定义拦截器修改异常传递逻辑,默认情况下它会把异常正常返回给CommandGateway。
  • 检查FailureLoggingCallback的实现,确保它只做日志记录,没有吞噬异常的逻辑——该回调仅负责记录,不会阻止异常传递给调用方。
关键注意点
  • Axon的CommandGateway方法会把领域异常统一包装为CommandExecutionException,必须解包才能拿到自定义的DomainException。
  • Spring Web不会自动处理CompletableFuture内部的异常,必须显式抛出异常才能触发全局异常处理器。

内容的提问来源于stack exchange,提问作者Double M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 10:31:14