Spring Boot统一异常处理方案
异常处理的目标不是把所有异常都变成“成功响应”,而是在保留正确 HTTP 语义的前提下,为客户端提供稳定、可理解、便于排查的错误信息。
在 Web 项目中,参数错误、业务规则冲突、资源不存在和系统故障都会以异常的形式出现。如果每个 Controller 都自行拼接错误响应,不仅代码重复,还会导致错误结构、状态码和日志口径不一致。本文基于 Spring Boot 3.x,使用 @RestControllerAdvice 构建统一异常处理方案,并讨论生产环境中容易忽略的边界。
环境说明:示例使用 Spring Boot 3.x、Spring Framework 6 和 Java 17,Web 类型为 Servlet。Spring Boot 3 使用
jakarta.*包名;如果项目仍是 Spring Boot 2.x,需要注意依赖与验证 API 的包名差异。
一、为什么需要统一异常处理
先看不使用统一处理时的代码:
@GetMapping("/users/{id}")
public Result<UserResponse> getUser(@PathVariable Long id) {
try {
UserResponse user = userService.getById(id);
return Result.success(user);
} catch (UserNotFoundException e) {
return Result.error(404, "用户不存在");
} catch (Exception e) {
log.error("查询用户失败", e);
return Result.error(500, "系统异常");
}
}这种方式很快会出现几个问题:
每个 Controller 都重复编写
try-catch。有的接口返回 404,有的接口可能把“不存在”也返回成 200。
错误响应字段不统一,前端需要针对不同接口分别处理。
业务异常与系统异常混在一起,日志级别和排查方式不清晰。
直接在错误信息中返回异常堆栈,可能泄露内部实现。
统一异常处理将横切逻辑集中到全局处理器中,让 Controller 专注于正常业务流程:
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getById(id);
}用户不存在时,Service 直接抛出明确的业务异常,由全局处理器转换成一致的 HTTP 响应。
二、先统一错误响应结构
客户端处理错误时,通常需要稳定的错误码、可读消息、请求路径和追踪标识。可以定义如下结构:
import java.time.Instant;
public record ApiError(
Instant timestamp,
int status,
String code,
String message,
String path,
String traceId
) {
public static ApiError of(
int status,
String code,
String message,
String path,
String traceId
) {
return new ApiError(
Instant.now(), status, code, message, path, traceId
);
}
}字段含义如下:
一次用户不存在的响应可以是:
{
"timestamp": "2026-11-09T08:00:00Z",
"status": 404,
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"path": "/api/users/1001",
"traceId": "4f8c0a1d7f2c"
}code 应该保持稳定,供客户端编写程序逻辑;message 面向人阅读,可以优化文案,但不应被前端当作程序分支判断依据。
如果项目已经存在统一返回包装,也可以复用现有结构,不必为了采用本文示例而同时维护两套响应模型。
三、定义业务异常与错误码
3.1 错误码枚举
将常用错误码集中管理,避免字符串散落在代码中:
import org.springframework.http.HttpStatus;
public enum ErrorCode {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "用户不存在"),
USER_DISABLED(HttpStatus.CONFLICT, "用户已被禁用"),
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "请求参数不合法"),
INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "系统暂时不可用");
private final HttpStatus status;
private final String defaultMessage;
ErrorCode(HttpStatus status, String defaultMessage) {
this.status = status;
this.defaultMessage = defaultMessage;
}
public HttpStatus getStatus() {
return status;
}
public String getDefaultMessage() {
return defaultMessage;
}
}HTTP 状态码表达错误的通用语义,业务错误码表达更具体的原因。例如,多个业务异常都可以使用 409,但分别对应“用户已存在”“库存不足”或“状态冲突”。
3.2 业务异常基类
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getDefaultMessage());
this.errorCode = errorCode;
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}业务异常继承 RuntimeException,Service 不需要在每个方法签名中声明。是否使用受检异常应根据项目约定决定,但不要在 Service 中为了绕过异常处理而随意捕获并吞掉异常。
需要动态提示时,可以传入更具体的消息:
throw new BusinessException(
ErrorCode.USER_DISABLED,
"当前账号已被禁用,请联系管理员"
);动态消息仍然应当是可以展示给客户端的内容,不要拼接 SQL、密钥或内部堆栈。
四、使用 @RestControllerAdvice 统一处理
@RestControllerAdvice 结合了 @ControllerAdvice 和 @ResponseBody,可以集中处理一组 Controller 抛出的异常,并将返回值写入响应体。
先实现业务异常和兜底异常两个处理器:
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log =
LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiError> handleBusinessException(
BusinessException ex,
HttpServletRequest request
) {
ErrorCode errorCode = ex.getErrorCode();
int status = errorCode.getStatus().value();
log.warn("业务异常:code={}, path={}, message={}",
errorCode.name(), request.getRequestURI(), ex.getMessage());
return ResponseEntity
.status(errorCode.getStatus())
.body(ApiError.of(
status,
errorCode.name(),
ex.getMessage(),
request.getRequestURI(),
TraceContext.getTraceId()
));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiError> handleException(
Exception ex,
HttpServletRequest request
) {
log.error("未处理异常:path={}", request.getRequestURI(), ex);
return ResponseEntity
.internalServerError()
.body(ApiError.of(
500,
ErrorCode.INTERNAL_ERROR.name(),
ErrorCode.INTERNAL_ERROR.getDefaultMessage(),
request.getRequestURI(),
TraceContext.getTraceId()
));
}
}关键点有三个:
使用
ResponseEntity设置真实 HTTP 状态码,不把 404、400 和 500 都包装成 200。业务异常记录简明日志,未知异常记录完整堆栈。
500 响应对外使用通用文案,详细错误只留在受保护的日志系统中。
@ExceptionHandler(Exception.class) 是最后一道兜底,不应成为忽略具体异常的理由。客户端能够修正的问题,应尽量映射到准确的 4xx 状态码。
五、补齐常见 Web 异常
仅处理业务异常和 Exception 还不够。Spring MVC 在参数解析、请求体转换和路由匹配阶段也会抛出异常。
5.1 请求参数缺失与类型转换失败
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiError> handleMissingParameter(
MissingServletRequestParameterException ex,
HttpServletRequest request
) {
String message = "缺少请求参数:" + ex.getParameterName();
return badRequest(message, request);
}
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiError> handleTypeMismatch(
MethodArgumentTypeMismatchException ex,
HttpServletRequest request
) {
String message = "请求参数格式错误:" + ex.getName();
return badRequest(message, request);
}
private ResponseEntity<ApiError> badRequest(
String message,
HttpServletRequest request
) {
return ResponseEntity.badRequest().body(ApiError.of(
HttpStatus.BAD_REQUEST.value(),
ErrorCode.INVALID_PARAMETER.name(),
message,
request.getRequestURI(),
TraceContext.getTraceId()
));
}例如,接口期望 page 为整数,客户端却传入 page=abc,就返回 400,而不是让类型转换异常落入 500。
5.2 JSON 请求体无法读取
import org.springframework.http.converter.HttpMessageNotReadableException;
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiError> handleNotReadable(
HttpMessageNotReadableException ex,
HttpServletRequest request
) {
log.warn("请求体解析失败:path={}, reason={}",
request.getRequestURI(), ex.getMessage());
return badRequest("请求体不是合法的 JSON,或字段类型不匹配", request);
}不要把 Jackson 的完整异常消息直接返回给客户端,其中可能包含类名、字段结构和内部实现信息。
5.3 请求方法不支持
import org.springframework.web.HttpRequestMethodNotSupportedException;
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ApiError> handleMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpServletRequest request
) {
return ResponseEntity
.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiError.of(
405,
"METHOD_NOT_ALLOWED",
"请求方法不支持",
request.getRequestURI(),
TraceContext.getTraceId()
));
}如果客户端向只支持 GET 的路径发送 DELETE,返回 405 比返回 500 更准确。
5.4 静态资源或路径不存在
Spring Framework 6.1 及以上版本中,未匹配到资源的请求可能抛出 NoResourceFoundException:
import org.springframework.web.servlet.resource.NoResourceFoundException;
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ApiError> handleNotFound(
NoResourceFoundException ex,
HttpServletRequest request
) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiError.of(
404,
"RESOURCE_NOT_FOUND",
"请求的资源不存在",
request.getRequestURI(),
TraceContext.getTraceId()
));
}不同 Spring Boot 版本、静态资源配置和容器错误分发方式可能影响 404 的处理路径。应以实际项目版本为准,并用请求验证响应结构,而不是假设所有 404 都一定进入同一个处理器。
六、参数校验异常如何转换
下一篇会系统介绍参数校验,这里先统一其错误输出,避免校验失败时又回到默认响应。
6.1 @RequestBody 对象校验失败
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.web.bind.MethodArgumentNotValidException;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
Map<String, String> fields = new LinkedHashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
fields.putIfAbsent(error.getField(), error.getDefaultMessage())
);
String message = fields.isEmpty()
? ErrorCode.INVALID_PARAMETER.getDefaultMessage()
: fields.toString();
log.warn("请求体校验失败:path={}, fields={}",
request.getRequestURI(), fields);
return badRequest(message, request);
}直接返回 fields.toString() 能完成演示,但生产项目更适合为字段错误增加独立结构:
public record FieldViolation(
String field,
String message
) {}然后让错误响应包含 List<FieldViolation> errors。这样前端不需要解析字符串,可以稳定地定位每个输入项。
6.2 方法参数校验失败
对 @RequestParam、@PathVariable 使用约束注解时,需要处理 ConstraintViolationException:
import jakarta.validation.ConstraintViolationException;
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiError> handleConstraintViolation(
ConstraintViolationException ex,
HttpServletRequest request
) {
String message = ex.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath()
+ ": " + violation.getMessage())
.distinct()
.reduce((left, right) -> left + "; " + right)
.orElse(ErrorCode.INVALID_PARAMETER.getDefaultMessage());
return badRequest(message, request);
}如果项目还使用 @ModelAttribute 或表单绑定,应根据实际情况处理 BindException。HandlerMethodValidationException 则可能出现在较新的 Spring Framework 方法校验流程中,具体异常类型应结合版本和配置确认。
七、完整调用链示例
7.1 Service 只表达业务规则
import org.springframework.stereotype.Service;
@Service
public class UserService {
public UserResponse getById(Long id) {
if (id == null || id <= 0) {
throw new BusinessException(
ErrorCode.INVALID_PARAMETER,
"用户 ID 必须为正整数"
);
}
if (!exists(id)) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return new UserResponse(id, "洋洋", true);
}
private boolean exists(Long id) {
// 示例仅保留一个存在的数据,实际项目应查询数据库
return id == 1L;
}
}public record UserResponse(
Long id,
String nickname,
Boolean enabled
) {}真实项目中,exists 和查询通常应合并为一次数据库操作,避免“先判断再查询”产生多余请求和并发间隙。这里拆开只是为了突出异常分支。
7.2 Controller 不再捕获业务异常
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserResponse getUser(@PathVariable Long id) {
return userService.getById(id);
}
}请求 GET /api/users/1 返回正常数据;请求 GET /api/users/1001 返回 404;请求 GET /api/users/abc 在参数转换阶段返回 400。
可以使用 curl 验证:
curl -i http://localhost:8080/api/users/1001应看到:
HTTP/1.1 404
Content-Type: application/json{
"timestamp": "2026-11-09T08:00:00Z",
"status": 404,
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"path": "/api/users/1001",
"traceId": "4f8c0a1d7f2c"
}八、追踪 ID 与日志规范
统一异常响应中的 traceId 不能只是一个从未生成的字段。可以基于 SLF4J 的 MDC 保存当前请求的追踪标识。
8.1 通过过滤器设置 MDC
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
public static final String TRACE_ID = "traceId";
public static final String HEADER = "X-Trace-Id";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String traceId = request.getHeader(HEADER);
if (traceId == null || traceId.isBlank()) {
traceId = UUID.randomUUID().toString()
.replace("-", "")
.substring(0, 16);
}
try {
MDC.put(TRACE_ID, traceId);
response.setHeader(HEADER, traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}生产环境若接收上游 traceId,还应限制长度和字符集,避免不受信任的请求头污染日志。已经接入 Micrometer Tracing、OpenTelemetry 或 SkyWalking 时,应优先复用现有追踪体系,而不是另建一套互不关联的 ID。
8.2 封装上下文读取
import org.slf4j.MDC;
public final class TraceContext {
private TraceContext() {
}
public static String getTraceId() {
return MDC.get(TraceIdFilter.TRACE_ID);
}
}在 application.yml 中把追踪 ID 加入日志格式:
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{traceId:-}] %logger{36} - %msg%n"finally 中清理 MDC 非常重要。Servlet 容器会复用线程,不清理可能让后续请求错误地沿用旧追踪 ID。
8.3 日志级别建议
不要同时在 Service、Controller 和全局处理器重复记录同一异常堆栈,否则一次故障会产生多条相似 ERROR 日志。
九、局部异常处理与作用域控制
@RestControllerAdvice 默认作用于多个 Controller,也可以通过包、注解或指定类限定范围:
@RestControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler {
// 只处理 com.example.api 包下的 Controller
}如果确实只需要处理当前 Controller 的少量异常,可以在 Controller 内使用 @ExceptionHandler。局部处理器优先级更高,全局处理器负责公共兜底。
还可以使用 @Order 调整多个 Advice 的顺序:
import org.springframework.core.annotation.Order;
@Order(1)
@RestControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler {
}@Order(2)
@RestControllerAdvice
public class GlobalExceptionHandler {
}但更好的方式是减少职责重叠:API 异常处理器负责接口,视图异常处理器负责页面,全局处理器只处理真正公共的场景。避免多个 Advice 同时声明同一个异常类型,导致选择规则难以维护。
十、使用 ProblemDetail 表达标准问题详情
Spring Framework 6 支持 ProblemDetail,对应 RFC 9457 所更新的 Problem Details for HTTP APIs 规范(早期常称 RFC 7807)。如果项目没有历史响应格式约束,可以直接采用它,减少自定义模型。
import jakarta.servlet.http.HttpServletRequest;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ProblemDetailExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ProblemDetail> handleBusiness(
BusinessException ex,
HttpServletRequest request
) {
HttpStatus status = ex.getErrorCode().getStatus();
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
status,
ex.getMessage()
);
problem.setTitle("请求处理失败");
problem.setType(URI.create("about:blank"));
problem.setProperty("code", ex.getErrorCode().name());
problem.setProperty("traceId", TraceContext.getTraceId());
problem.setProperty("path", request.getRequestURI());
return ResponseEntity.status(status).body(problem);
}
}典型响应包含:
{
"type": "about:blank",
"title": "请求处理失败",
"status": 404,
"detail": "用户不存在",
"code": "USER_NOT_FOUND",
"traceId": "4f8c0a1d7f2c",
"path": "/api/users/1001"
}自定义 ApiError 与 ProblemDetail 都能完成统一异常处理。选择时重点考虑团队规范、客户端兼容和现有接口契约,不要在同一套 API 中随意混用两种错误格式。
十一、几个容易踩坑的边界
11.1 不要捕获后返回 HTTP 200
以下做法会让监控、网关和客户端重试机制无法正确识别失败:
// 不推荐
return ResponseEntity.ok(ApiError.of(500, "INTERNAL_ERROR", ...));如果业务确实失败,就应返回匹配语义的 HTTP 状态码。特殊渠道要求始终返回 200 时,也应单独定义协议,不要让整个 Web API 默认失去状态码语义。
11.2 不要向客户端返回堆栈和内部异常消息
// 不推荐
body.setMessage(ex.getMessage());
body.setStackTrace(ex.getStackTrace());数据库异常可能包含 SQL、表名和连接信息,框架异常可能暴露类路径。对未知异常返回通用提示,详细信息写入受控日志。
11.3 注意 Spring Security 异常
@RestControllerAdvice 主要处理进入 Controller 及其调用链的异常。Spring Security 的认证入口、过滤器异常通常应由 AuthenticationEntryPoint、AccessDeniedHandler 等安全组件处理。
如果项目引入 Spring Security,不要让 Exception 兜底意外吞掉需要交由安全流程处理的异常。应通过实际请求验证 401、403 的响应格式,并统一安全层与业务层的错误模型。
11.4 异步请求需要单独验证
Callable、DeferredResult、WebFlux 或 Servlet 异步错误的处理路径可能与同步 MVC 请求不同。本文示例面向同步 Spring MVC;使用其他编程模型时,应针对对应异常处理机制补充配置和测试。
11.5 响应已经开始写出后无法可靠重写
如果异常发生在响应提交之后,例如流式输出中途失败,全局处理器可能无法再修改状态码和响应体。此时重点是记录日志、终止响应并监控连接中断,不能假设 Advice 可以修复所有输出异常。
11.6 文件上传、网关和容器错误不一定经过 Controller
请求大小超过网关限制、Tomcat 在到达应用前拒绝请求、负载均衡器超时等情况,都不会由 Controller Advice 处理。需要在网关、容器和基础设施层保持兼容的错误格式与追踪策略。
十二、用 MockMvc 验证异常契约
异常处理属于 API 契约,应该通过测试固定状态码和关键字段。以下是一个针对用户查询接口的切片测试思路:
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
when(userService.getById(1001L))
.thenThrow(new BusinessException(ErrorCode.USER_NOT_FOUND));
mockMvc.perform(get("/api/users/{id}", 1001L))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.code").value("USER_NOT_FOUND"))
.andExpect(jsonPath("$.path").value("/api/users/1001"));
}
@Test
void shouldReturnBadRequestWhenIdIsNotNumber() throws Exception {
mockMvc.perform(get("/api/users/abc"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("INVALID_PARAMETER"));
}
}@MockBean 在较新的 Spring Boot 版本中已被标记弃用,可迁移到 @MockitoBean,具体以项目使用的测试版本为准。若应用启用了 Spring Security,切片测试还需要配置测试用户或安全测试支持。
至少覆盖以下场景:
正常响应不被全局处理器改变;
业务异常映射到正确状态码和业务错误码;
参数转换失败返回 400;
JSON 解析失败返回 400;
未知异常返回 500,且响应不包含堆栈;
错误响应包含可关联日志的追踪 ID。
十三、推荐落地顺序
在已有项目中改造时,可以按以下顺序逐步实施:
先统计现有错误响应,确定客户端依赖的字段和状态码。
建立统一错误模型,保留必要的兼容字段。
定义少量、稳定的业务错误码,避免为每句话创建一个错误码。
引入
@RestControllerAdvice,先处理业务异常与未知异常。补齐参数解析、JSON 转换、方法不支持等常见 MVC 异常。
统一认证、权限、网关和容器层的错误边界。
接入请求追踪 ID,清理重复日志和敏感信息。
使用 MockMvc 与真实 HTTP 请求验证契约,再逐步删除 Controller 中重复的
try-catch。
不要一次性把每个异常都映射成大量细碎处理器。先覆盖公共异常,再根据实际调用链和监控发现补齐,结构更容易维护。
十四、总结
一套可维护的 Spring Boot 统一异常处理方案应做到:
Controller 专注正常业务流程,Service 用明确异常表达业务失败。
错误响应包含稳定业务错误码、可读消息、请求路径和追踪 ID。
使用真实 HTTP 状态码区分客户端错误、业务冲突和系统故障。
@RestControllerAdvice集中处理公共异常,具体异常优先于通用兜底。参数错误返回 400,未知异常返回 500,不把所有失败包装成 200。
业务异常日志简明,未知异常保留完整堆栈,客户端不接触内部细节。
MDC 必须清理,已有分布式追踪体系时直接复用。
自定义
ApiError与标准ProblemDetail按项目契约二选一。Security、异步请求、网关与响应提交后的异常需要单独考虑。
用测试固定异常响应,防止后续升级破坏客户端依赖的错误格式。
统一异常处理不是简单地“把异常转成 JSON”,而是建立一套稳定的 API 失败契约,并让客户端提示、HTTP 语义和日志排查形成闭环。
下一站预告
下一篇《Spring Boot参数校验与自定义校验》将介绍 Bean Validation、分组校验、嵌套对象校验和自定义约束注解,并与本文的统一异常处理组合成完整的请求校验流程。
作者:洋洋技术笔记 发布日期:2026-11-09 系列:Spring Boot实战 - 第7篇
原文链接:洋洋的技术笔记
评论区