Spring Boot: 全局异常处理

最后更新:2026-08-26

全局异常处理让 API 错误响应格式统一——客户端不再面对五花八门的错误格式,调试和对接都更高效。

1. 你将学到


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) 异常处理分发流程

100%
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) 异常类层次结构

100%
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)。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 实现 GlobalExceptionHandler,处理 ResourceNotFoundException 和 BusinessException,返回统一的 ErrorResponse 格式。

  2. 进阶题(难度⭐⭐):添加 MethodArgumentNotValidException 处理器,提取字段级错误详情。实现 traceId 生成并在日志中使用。

  3. 挑战题(难度⭐⭐⭐):创建一个 Servlet Filter 在请求入口生成 traceId 并设置到 MDC,让所有日志自动携带 traceId,GlobalExceptionHandler 从 MDC 读取 traceId,实现全链路日志追踪。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏