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

Spring异步REST服务中CompletableFuture异常捕获及自定义异常传递问题

问题分析与解决方案

咱们先直击核心问题:当服务端抛出自定义RuntimeException时,Spring的AsyncRestTemplate对非2xx的HTTP状态码会自动触发错误拦截逻辑,把响应包装成HttpServerErrorException(5xx)或HttpClientErrorException(4xx),这就是你换状态码到400、500时拿不到自定义消息的原因。下面一步步给你解决:

一、服务端:用标准JSON响应返回自定义异常信息

你之前用response.sendError(200, "custom Error")能生效,是因为200属于成功状态码,AsyncRestTemplate不会把它当成错误处理,但这不符合HTTP语义(异常场景应该用非2xx状态码)。正确的做法是把异常信息封装成标准JSON响应:

1. 定义统一异常响应类

先创建一个用来标准化错误输出的类:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class ErrorResponse {
    private String timestamp;
    private int status;
    private String error;
    private String message;
    private String path;

    public ErrorResponse(int status, String error, String message, String path) {
        this.timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
        this.status = status;
        this.error = error;
        this.message = message;
        this.path = path;
    }

    // 省略getter/setter方法
}

2. 修改异常处理方法

把原来的sendError改成返回这个异常响应体,同时指定合法的非2xx状态码:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;

@RestControllerAdvice // 全局异常处理,比单个控制器的@ExceptionHandler更通用
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException ex, HttpServletRequest request) {
        ErrorResponse errorResponse = new ErrorResponse(
                HttpStatus.BAD_REQUEST.value(), // 比如用400状态码
                HttpStatus.BAD_REQUEST.getReasonPhrase(),
                ex.getMessage(), // 这里就是你的自定义异常消息
                request.getRequestURI()
        );
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

二、客户端:从异常中解析自定义消息

现在服务端返回的是标准JSON错误响应,但AsyncRestTemplate还是会把非2xx状态码包装成异常,我们需要从异常里解析出响应体的自定义消息:

修改客户端的getResult方法:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;

import java.util.concurrent.ExecutionException;

@GetMapping("/result")
public Object getResult() {
    try {
        return entity.get();
    } catch (InterruptedException | ExecutionException e) {
        // 拆解ExecutionException的实际异常
        Throwable cause = e.getCause();
        ObjectMapper mapper = new ObjectMapper();
        
        if (cause instanceof HttpServerErrorException) {
            HttpServerErrorException serverError = (HttpServerErrorException) cause;
            try {
                ErrorResponse errorResponse = mapper.readValue(serverError.getResponseBodyAsString(), ErrorResponse.class);
                return errorResponse.getMessage(); // 返回自定义异常消息
            } catch (JsonProcessingException jsonEx) {
                throw new RuntimeException("解析错误响应失败", jsonEx);
            }
        } else if (cause instanceof HttpClientErrorException) {
            HttpClientErrorException clientError = (HttpClientErrorException) cause;
            try {
                ErrorResponse errorResponse = mapper.readValue(clientError.getResponseBodyAsString(), ErrorResponse.class);
                return errorResponse.getMessage(); // 返回自定义异常消息
            } catch (JsonProcessingException jsonEx) {
                throw new RuntimeException("解析错误响应失败", jsonEx);
            }
        } else {
            throw new RuntimeException("未知错误", cause);
        }
    }
}

注意:客户端需要和服务端保持ErrorResponse类一致,同时确保项目引入了Jackson依赖(Spring Boot默认已包含)。

三、额外优化建议

  1. 复用AsyncRestTemplate实例:不要在callService里每次创建AsyncRestTemplate,把它注册成Bean复用:
@Bean
public AsyncRestTemplate asyncRestTemplate() {
    return new AsyncRestTemplate();
}

然后在客户端控制器中注入使用:

@Autowired
private AsyncRestTemplate asyncRestTemplate;
  1. 服务端Async方法的异常处理:虽然Spring的@Async会自动把方法抛出的异常包装到CompletableFuture里,但显式处理更清晰:
@GetMapping("/start")
@Async
public CompletableFuture<String> callService() throws InterruptedException{
    Thread.sleep(10000L);
    if(true) {
        CompletableFuture<String> future = new CompletableFuture<>();
        future.completeExceptionally(new RuntimeException("Custom Error"));
        return future;
    }
    return CompletableFuture.completedFuture("OK");
}

内容的提问来源于stack exchange,提问作者J. Abel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 07:43:56