Spring Boot: 全局异常处理
最后更新:2026-08-26
全局异常处理让 API 错误响应格式统一——客户端不再面对五花八门的错误格式,调试和对接都更高效。
1. 你将学到
@RestControllerAdvice+@ExceptionHandler全局异常捕获- 自定义业务异常体系:
BusinessException/ResourceNotFoundException/ValidationException - 错误响应 DTO 设计:code / message / timestamp / details
MethodArgumentNotValidException验证错误的特殊处理- 异常日志记录与错误追踪 ID 生成
2. 一个前端开发者的真实故事
(1) 痛点:错误响应格式混乱
Bob 是前端开发者,对接 OrderFlow API 时崩溃了:有的接口 404 返回纯文本 "Not Found",有的 500 返回 HTML 错误页面,有的业务异常返回 {"error": "xxx"},有的返回 {"message": "xxx"}。他不得不为每个接口写不同的错误处理逻辑,代码里到处是 try-catch,还经常遗漏导致页面白屏。
(2) @RestControllerAdvice 的解法
统一异常处理器让所有错误响应格式一致:
JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage(), Instant.now()));
}
}
(3) 收益
Alice 实现全局异常处理后,所有错误响应格式统一为 {code, message, timestamp, details},Bob 前端只需一个统一的错误处理函数,对接效率提升 5 倍。
3. @RestControllerAdvice 机制
(1) 异常处理分发流程
graph TD
A[Controller throws Exception] --> B{Spring DispatcherServlet}
B --> C["@RestControllerAdvice<br/>Scans @ExceptionHandler"]
C --> D{Match Exception Type?}
D -->|Yes| E["Execute @ExceptionHandler<br/>Return ErrorResponse"]
D -->|No| F["Spring Default<br/>Error Response"]
E --> G["Client receives<br/>Consistent JSON"]
F --> H["Client receives<br/>Inconsistent response"]
| 注解 | 用途 | 放置位置 |
|---|---|---|
@RestControllerAdvice |
全局异常处理类 | 类上 |
@ExceptionHandler |
处理指定异常类型 | 方法上 |
@ResponseStatus |
指定响应状态码 | 异常类或方法上 |
▶ 示例: 基础全局异常处理器
JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(
ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
"RESOURCE_NOT_FOUND", ex.getMessage(), Instant.now(), null);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex) {
ErrorResponse error = new ErrorResponse(
"BUSINESS_ERROR", ex.getMessage(), Instant.now(), null);
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR", "An unexpected error occurred", Instant.now(), null);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
4. 自定义业务异常体系
(1) 异常类层次结构
graph TD
A[RuntimeException] --> B[BusinessException<br/>Base business exception]
B --> C[ResourceNotFoundException<br/>404 Not Found]
B --> D[InsufficientStockException<br/>422 Business Rule Violation]
B --> E[OrderStateException<br/>422 Invalid State Transition]
A --> F[ValidationException<br/>400 Bad Request]
▶ 示例: 自定义异常类
JAVA
// Base business exception
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// Resource not found
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resource, Long id) {
super("RESOURCE_NOT_FOUND",
resource + " not found with id: " + id);
}
}
// Insufficient stock
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(Long productId, int available, int requested) {
super("INSUFFICIENT_STOCK",
String.format("Product %d: available=%d, requested=%d",
productId, available, requested));
}
}
// Invalid order state
public class OrderStateException extends BusinessException {
public OrderStateException(Long orderId, String current, String target) {
super("INVALID_ORDER_STATE",
String.format("Order %d: cannot transition from %s to %s",
orderId, current, target));
}
}
输出:
TEXT
📖 仅展示
// 执行成功
5. 错误响应 DTO 设计
(1) ErrorResponse 设计原则
▶ 示例: ErrorResponse 和 ValidationErrorResponse
JAVA
public record ErrorResponse(
String code,
String message,
Instant timestamp,
String traceId,
List<FieldError> details
) {
public record FieldError(
String field,
String message,
Object rejectedValue
) {}
}
// Convenience factory methods
public class ErrorResponse {
public static ErrorResponse of(String code, String message, String traceId) {
return new ErrorResponse(code, message, Instant.now(), traceId, null);
}
public static ErrorResponse withDetails(String code, String message,
String traceId, List<FieldError> details) {
return new ErrorResponse(code, message, Instant.now(), traceId, details);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| 字段 | 类型 | 说明 |
|---|---|---|
code |
String | 错误码(机器可读) |
message |
String | 错误消息(人类可读) |
timestamp |
Instant | 发生时间 |
traceId |
String | 追踪 ID(关联日志) |
details |
List | 字段级错误详情(验证错误用) |
6. 验证错误特殊处理
▶ 示例: MethodArgumentNotValidException 处理
JAVA
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(
MethodArgumentNotValidException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
List<ErrorResponse.FieldError> details = ex.getBindingResult()
.getFieldErrors().stream()
.map(fe -> new ErrorResponse.FieldError(
fe.getField(),
fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
fe.getRejectedValue()
))
.toList();
ErrorResponse error = ErrorResponse.withDetails(
"VALIDATION_ERROR",
"Input validation failed",
traceId,
details
);
log.warn("Validation failed [traceId={}]: {}", traceId, details);
return ResponseEntity.badRequest().body(error);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
💻 输出:
JSON
{
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"timestamp": "2024-01-15T10:00:00Z",
"traceId": "abc-123-def",
"details": [
{"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": 0},
{"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
]
}
7. 异常日志与追踪 ID
▶ 示例: 带追踪 ID 的完整异常处理器
JAVA
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Resource not found [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ErrorResponse> handleInsufficientStock(
InsufficientStockException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Insufficient stock [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.warn("Business error [traceId={}]: {}", traceId, ex.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), traceId));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(
Exception ex,
HttpServletRequest request) {
String traceId = generateTraceId();
log.error("Unexpected error [traceId={}]", traceId, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("INTERNAL_ERROR",
"An unexpected error occurred. TraceId: " + traceId, traceId));
}
private String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
| 异常类型 | HTTP 状态码 | 日志级别 | 说明 |
|---|---|---|---|
ResourceNotFoundException |
404 | WARN | 资源不存在,客户端问题 |
InsufficientStockException |
409 | WARN | 业务冲突 |
BusinessException |
422 | WARN | 业务规则违反 |
MethodArgumentNotValidException |
400 | WARN | 输入校验失败 |
Exception |
500 | ERROR | 未预期错误,需排查 |
8. 综合示例:OrderFlow 完整异常处理体系
JAVA
// ErrorResponse.java
package com.orderflow.exception;
import java.time.Instant;
import java.util.List;
public record ErrorResponse(
String code, String message, Instant timestamp,
String traceId, List<FieldError> details
) {
public record FieldError(String field, String message, Object rejectedValue) {}
public static ErrorResponse of(String code, String message, String traceId) {
return new ErrorResponse(code, message, Instant.now(), traceId, null);
}
public static ErrorResponse withDetails(String code, String message,
String traceId, List<FieldError> details) {
return new ErrorResponse(code, message, Instant.now(), traceId, details);
}
}
// BusinessException hierarchy
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message); this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resource, Long id) {
super("RESOURCE_NOT_FOUND", resource + " not found with id: " + id);
}
}
public class InsufficientStockException extends BusinessException {
public InsufficientStockException(Long productId, int avail, int req) {
super("INSUFFICIENT_STOCK",
"Product " + productId + ": available=" + avail + ", requested=" + req);
}
}
// GlobalExceptionHandler.java
package com.orderflow.exception;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
String tid = tid();
log.warn("[{}] {}", tid, ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
}
@ExceptionHandler(InsufficientStockException.class)
public ResponseEntity<ErrorResponse> handleStock(InsufficientStockException ex) {
String tid = tid();
log.warn("[{}] {}", tid, ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage(), tid));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
String tid = tid();
List<ErrorResponse.FieldError> details = ex.getBindingResult()
.getFieldErrors().stream()
.map(f -> new ErrorResponse.FieldError(f.getField(),
f.getDefaultMessage() != null ? f.getDefaultMessage() : "", f.getRejectedValue()))
.toList();
log.warn("[{}] Validation failed: {}", tid, details);
return ResponseEntity.badRequest()
.body(ErrorResponse.withDetails("VALIDATION_ERROR", "Validation failed", tid, details));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
String tid = tid();
log.error("[{}] Unexpected error", tid, ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse.of("INTERNAL_ERROR", "Unexpected error. Ref: " + tid, tid));
}
private String tid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
}
❓ 常见问题
Q @ControllerAdvice 和 @RestControllerAdvice 有什么区别?
A @RestControllerAdvice = @ControllerAdvice + @ResponseBody。REST API 项目一律用 @RestControllerAdvice,方法返回值自动序列化为 JSON。
Q @ExceptionHandler 匹配顺序是怎样的?
A Spring 选择最精确的异常类型匹配。如果同时注册了 BusinessException 和 Exception 的处理器,抛出 BusinessException 时优先匹配 BusinessException 处理器。
Q 多个 @RestControllerAdvice 类如何共存?
A 可以用 @Order 控制优先级。@Order(Ordered.HIGHEST_PRECEDENCE) 优先匹配。不同 Advice 类可以处理不同类型的异常。
Q 生产环境应该返回异常堆栈吗?
A 不应该。生产环境只返回错误码和通用消息,不暴露内部实现。堆栈信息只记录在日志中。对于未预期异常,返回 traceId 让运维通过日志定位。
Q 如何处理 Spring Security 的异常?
A Spring Security 异常(如 AccessDeniedException)在过滤器链中抛出,不走 @RestControllerAdvice。需要自定义 AuthenticationEntryPoint 和 AccessDeniedHandler。
Q traceId 和 MDC 有什么关系?
A traceId 用于响应给客户端关联日志,MDC 用于日志框架内关联同一次请求的所有日志。推荐两者使用相同的值,在 Filter 中设置 MDC.put("traceId", id)。
📖 小节
@RestControllerAdvice+@ExceptionHandler统一处理所有 Controller 异常- 自定义异常体系:BusinessException 为基类,子类区分不同业务场景
- ErrorResponse 包含 code、message、timestamp、traceId、details 五个字段
- 验证错误特殊处理:提取字段级错误详情到 details 数组
- 异常日志用 WARN/ERROR 区分业务异常和系统异常
- traceId 关联响应与日志,方便排查问题
📝 作业
-
基础题(难度⭐):为 OrderFlow 实现 GlobalExceptionHandler,处理 ResourceNotFoundException 和 BusinessException,返回统一的 ErrorResponse 格式。
-
进阶题(难度⭐⭐):添加 MethodArgumentNotValidException 处理器,提取字段级错误详情。实现 traceId 生成并在日志中使用。
-
挑战题(难度⭐⭐⭐):创建一个 Servlet Filter 在请求入口生成 traceId 并设置到 MDC,让所有日志自动携带 traceId,GlobalExceptionHandler 从 MDC 读取 traceId,实现全链路日志追踪。