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默认已包含)。
三、额外优化建议
- 复用AsyncRestTemplate实例:不要在
callService里每次创建AsyncRestTemplate,把它注册成Bean复用:
@Bean public AsyncRestTemplate asyncRestTemplate() { return new AsyncRestTemplate(); }
然后在客户端控制器中注入使用:
@Autowired private AsyncRestTemplate asyncRestTemplate;
- 服务端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
相关产品推荐
相关产品推荐

