一、核心概念

在 Spring Boot 应用中,当控制器方法执行过程中发生异常时,需要一种集中、统一、优雅的方式来处理这些异常,并向客户端返回结构化的错误信息(如 JSON),而不是默认的错误页面或堆栈信息。

1. 核心注解

注解 作用
@ControllerAdvice 全局异常处理器。一个带有此注解的类,其内部的 @ExceptionHandler 方法可以处理整个应用中所有控制器抛出的异常。它是 @Component 的特殊形式,会被 Spring 扫描并注册为全局的异常处理切面。
@ExceptionHandler 异常处理方法。标注在方法上,指定该方法用于处理哪些类型的异常(通过方法参数声明)。它可以出现在 @Controller 类中(处理该控制器的异常),但更常见的是与 @ControllerAdvice 结合使用,实现全局处理。

2. 处理流程

  1. 控制器方法执行,抛出异常。
  2. Spring MVC 的 DispatcherServlet 捕获异常。
  3. 查找匹配的 @ExceptionHandler 方法:
    • 首先在抛出异常的控制器类中查找。
    • 如果未找到,则在所有被 @ControllerAdvice 标注的类中查找。
  4. 执行找到的 @ExceptionHandler 方法。
  5. 该方法的返回值(通常是 ResponseEntity 或直接对象)被序列化并作为 HTTP 响应返回给客户端。

3. 常见异常类型

  • 自定义业务异常UserNotFoundException, OrderAlreadyPaidException 等。
  • Spring MVC 异常MissingServletRequestParameterException, HttpMessageNotReadableException, MethodArgumentNotValidException (参数校验失败)。
  • HTTP 状态码异常HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException
  • 通用异常Exception (捕获所有未被处理的异常)。

二、操作步骤(非常详细)

步骤 1:创建自定义业务异常(可选但推荐)

// 定义一个自定义异常类
public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
    // 可以添加更多字段,如 errorCode, timestamp 等
}

步骤 2:创建全局异常处理类

2.1 创建 @ControllerAdvice

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

// 标记为全局异常处理器组件
@ControllerAdvice
public class GlobalExceptionHandler {

    // 在这里添加 @ExceptionHandler 方法
}

2.2 处理自定义业务异常

@ControllerAdvice
public class GlobalExceptionHandler {

    // 处理 UserNotFoundException
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<Map<String, Object>> handleUserNotFound(UserNotFoundException ex) {
        Map<String, Object> errorDetails = new LinkedHashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("status", HttpStatus.NOT_FOUND.value());
        errorDetails.put("error", "Not Found");
        errorDetails.put("message", ex.getMessage());
        errorDetails.put("path", "/api/users"); // 可以从请求中获取实际路径

        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }
}

2.3 处理 Spring MVC 参数校验异常

import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // ... handleUserNotFound 方法

    // 处理 @Valid 校验失败抛出的 MethodArgumentNotValidException
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", HttpStatus.BAD_REQUEST.value());
        body.put("error", "Validation Failed");

        // 收集所有字段错误信息
        Map<String, String> errors = new LinkedHashMap<>();
        for (FieldError error : ex.getBindingResult().getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        body.put("errors", errors);

        return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
    }
}

2.4 处理缺少请求参数异常

import org.springframework.web.bind.MissingServletRequestParameterException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // ... 其他方法

    // 处理缺少必需的 @RequestParam 参数
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<Map<String, Object>> handleMissingParams(
            MissingServletRequestParameterException ex) {
        Map<String, Object> errorDetails = new LinkedHashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("status", HttpStatus.BAD_REQUEST.value());
        errorDetails.put("error", "Bad Request");
        errorDetails.put("message", "Required parameter '" + ex.getParameterName() + "' is missing");
        errorDetails.put("parameter", ex.getParameterName());

        return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
    }
}

2.5 处理请求体反序列化异常

import org.springframework.http.converter.HttpMessageNotReadableException;

@ControllerAdvice
public class GlobalExceptionHandler {

    // ... 其他方法

    // 处理 JSON 格式错误或无法反序列化
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadable(
            HttpMessageNotReadableException ex) {
        Map<String, Object> errorDetails = new LinkedHashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("status", HttpStatus.BAD_REQUEST.value());
        errorDetails.put("error", "Bad Request");
        errorDetails.put("message", "Malformed JSON request or invalid data format");
        // 注意:ex.getCause().getMessage() 可能包含敏感信息,生产环境需谨慎处理
        errorDetails.put("detail", ex.getMessage());

        return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
    }
}

2.6 处理所有未被捕获的异常(兜底)

@ControllerAdvice
public class GlobalExceptionHandler {

    // ... 其他更具体的异常处理方法

    // 处理所有其他未被上述方法捕获的异常
    // 通常放在最后,避免覆盖更具体的处理
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
        Map<String, Object> errorDetails = new LinkedHashMap<>();
        errorDetails.put("timestamp", LocalDateTime.now());
        errorDetails.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
        errorDetails.put("error", "Internal Server Error");
        errorDetails.put("message", "An unexpected error occurred");

        // **生产环境注意**:不要返回 ex.getMessage() 或堆栈,避免信息泄露
        // errorDetails.put("detail", ex.getMessage()); // 仅开发环境调试用

        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

步骤 3:在控制器中抛出异常

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // 模拟查找用户
        if (id <= 0) {
            throw new IllegalArgumentException("User ID must be positive");
        }
        // 模拟用户不存在
        if (id == 999) {
            throw new UserNotFoundException("User with ID " + id + " not found");
        }
        return new User(id, "John Doe", 30);
    }

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user, BindingResult result) {
        // 注意:@Valid 会自动触发校验,如果失败会抛出 MethodArgumentNotValidException
        // 这里不需要手动检查 result,GlobalExceptionHandler 会处理
        // ... 保存用户逻辑
        user.setId(System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

三、常见错误与解决方案

错误 原因 解决方案
@ControllerAdvice 类未生效 类未被 Spring 扫描到 确保类在主应用类(@SpringBootApplication)的包或其子包下,或使用 @ComponentScan 明确指定。确保类上有 @ControllerAdvice 注解。
异常未被处理,返回了默认错误页 @ExceptionHandler 方法未匹配到异常类型 检查 @ExceptionHandler 的参数类型是否与抛出的异常类型匹配(或其父类)。确保 @ControllerAdvice 类被正确加载。
@ExceptionHandler 方法返回 null 导致 404 方法没有返回值或返回 null @ExceptionHandler 方法必须返回一个值(如 ResponseEntityString、对象等),该值将作为响应体。确保返回了有效的响应。
兜底 Exception 处理器覆盖了特定处理器 @ExceptionHandler(Exception.class) 定义在特定异常处理器之前 将最通用的 Exception 处理器放在最后。Spring 会优先调用最具体的匹配(子类优先于父类)。
ResponseEntity 状态码错误 返回的 HttpStatus 与错误类型不符 根据错误类型选择合适的 HTTP 状态码(400, 404, 500 等)。
生产环境泄露堆栈信息 在响应中返回了 ex.getMessage()ex.getStackTrace() 生产环境绝对不要在响应体中返回详细的异常信息或堆栈。只返回通用的错误消息。

四、注意事项

  1. @ControllerAdvice 的扫描:它必须是一个被 Spring 管理的 Bean。通常放在主应用类的包下。
  2. 异常匹配顺序
    • 子类异常优先于父类异常。
    • 在同一个 @ControllerAdvice 类中,方法定义的顺序通常不影响匹配(基于类型精确匹配),但为清晰起见,建议将通用的 Exception 放在最后。
  3. @ExceptionHandler 的返回值
    • 可以是 ResponseEntity<T>(最灵活,可控制状态码和头信息)。
    • 可以是直接的对象(如 Map<String, Object>),其状态码默认为 200,除非方法上有 @ResponseStatus
    • 可以是 String(通常用于视图名,但在 @RestControllerAdvice 中会作为响应体)。
  4. @ResponseStatus 注解:可以直接在异常类上使用 @ResponseStatus 来指定 HTTP 状态码,这样即使没有 @ExceptionHandler,Spring 也会返回该状态码。
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public class UserNotFoundException extends RuntimeException { ... }
    
  5. @RestControllerAdvice:这是 @ControllerAdvice + @ResponseBody 的组合。使用它时,@ExceptionHandler 方法的返回值会自动序列化为 JSON/XML,无需再加 @ResponseBody推荐用于 REST API
    @RestControllerAdvice // 推荐用于 REST API
    public class GlobalExceptionHandler {
        @ExceptionHandler(UserNotFoundException.class)
        public Map<String, Object> handleUserNotFound(...) { // 返回 Map 会自动转为 JSON
            return errorDetails;
        }
    }
    
  6. 安全性:永远不要在生产环境的错误响应中暴露敏感信息(数据库连接错误、文件路径、完整堆栈跟踪)。

五、使用技巧

  1. @RestControllerAdvice:对于纯 REST API,使用 @RestControllerAdvice 可以省去在每个 @ExceptionHandler 方法上加 @ResponseBody 的麻烦。
  2. 提取公共逻辑:创建一个基类或工具方法来构建标准化的错误响应体 Map
  3. 处理多种异常:一个 @ExceptionHandler 方法可以处理多个异常类型。
    @ExceptionHandler({UserNotFoundException.class, OrderNotFoundException.class})
    public ResponseEntity<?> handleNotFound(RuntimeException ex) {
        // ...
    }
    
  4. 访问原始请求信息:可以在 @ExceptionHandler 方法参数中注入 HttpServletRequestWebRequest 来获取请求的详细信息(如 URL、方法、头信息),用于日志记录或响应中。
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleGeneric(HttpServletRequest request, Exception ex) {
        // log.error("Error on request: {} {}", request.getMethod(), request.getRequestURI(), ex);
        // ...
    }
    
  5. 与日志集成:在 @ExceptionHandler 方法中记录详细的错误日志(包含堆栈),但只向客户端返回精简信息。
  6. 自定义异常包含元数据:在自定义异常类中添加 errorCode, timestamp 等字段,在处理器中使用它们。

六、最佳实践

  1. 使用 @RestControllerAdvice:对于现代 RESTful 服务,它是标准选择。
  2. 分层处理
    • 全局处理:使用 @ControllerAdvice 处理通用的、跨领域的异常(参数校验、资源未找到、服务器错误)。
    • 局部处理:在特定的 @Controller 中使用 @ExceptionHandler 处理该控制器特有的、需要特殊响应逻辑的异常。
  3. 标准化错误响应格式:定义一个统一的错误响应结构(如包含 timestamp, status, error, message, path, errors 等字段),所有 @ExceptionHandler 都遵循此格式。
  4. 优先处理具体异常:先处理具体的业务异常和 Spring 异常,最后用 Exception.class 作为兜底。
  5. 强制输入校验:结合 @ValidMethodArgumentNotValidException 处理器,确保数据入口的合法性。
  6. 有意义的错误消息:向客户端返回清晰、用户(或调用者)友好的错误信息,避免技术术语。
  7. 生产环境安全:严格控制错误信息的暴露,防止信息泄露。
  8. 全面的日志记录:确保所有异常(尤其是 500 错误)都被详细记录到日志中,便于排查。

七、性能优化

  1. 异常处理本身开销极小@ControllerAdvice 是基于 Spring AOP 代理的,其匹配和调用开销在正常请求处理中可以忽略不计。
  2. 避免在异常处理中进行耗时操作
    • 不要在 @ExceptionHandler 方法中执行复杂的数据库查询或远程调用。
    • 记录日志是主要的“耗时”操作,但这是必要的。确保日志框架配置合理(如异步日志)。
  3. 减少异常发生:性能优化的关键在于预防异常,而不是优化异常处理。确保代码健壮,做好输入校验和边界检查。
  4. @ResponseStatus 的效率:直接在异常上使用 @ResponseStatus 是最轻量级的方式,因为它由 Spring 底层直接处理,无需调用额外的处理器方法。但对于需要返回复杂响应体的场景,@ExceptionHandler 更灵活。

总结

@ControllerAdvice@ExceptionHandler 是 Spring Boot 中实现全局、统一、结构化异常处理的黄金标准。它们将错误处理逻辑从各个控制器中抽离出来,极大地提高了代码的可维护性和 API 的健壮性。

快速掌握路径

  1. 创建 @RestControllerAdvice
  2. 添加几个关键的 @ExceptionHandler 方法:处理 MethodArgumentNotValidException (校验), RuntimeException (自定义业务异常), Exception (兜底)。
  3. 在控制器中故意抛出异常进行测试
  4. 使用 Postman/curl 观察返回的 JSON 错误响应
  5. 集成日志,确保异常被记录。
  6. 审查并标准化错误响应格式

遵循最佳实践,你就能构建出用户友好、易于调试且安全的 Web 服务。