Spring 全局异常处理机制:多个 @ControllerAdvice 与重复 @ExceptionHandler

Spring 全局异常处理机制:多个 @ControllerAdvice 与重复 @ExceptionHandler

Spring 的异常处理机制允许你通过多个 @ControllerAdvice 类来组织异常处理逻辑。当存在多个处理器处理相同异常时,Spring 会按照特定规则决定使用哪个处理器。

处理规则

  1. 优先级顺序

    • 控制器本地的 @ExceptionHandler 方法优先级最高
    • 然后是按 @ControllerAdvice 类的顺序
    • 最后是默认的 Spring 异常处理
  2. 相同异常处理的选择

    • 当多个 @ControllerAdvice 类中定义了相同的异常处理时,Spring 会选择第一个匹配的处理器
    • 可以通过 @Order 注解或实现 Ordered 接口来控制 @ControllerAdvice 的执行顺序

示例代码

场景描述

假设我们有两个全局异常处理器和一个控制器,都处理 IllegalArgumentException

java 复制代码
// 第一个全局异常处理器
@ControllerAdvice
@Order(1)  // 较低优先级
public class GlobalExceptionHandler1 {
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgument1(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("Global Handler 1: " + ex.getMessage());
    }
}

// 第二个全局异常处理器
@ControllerAdvice
@Order(0)  // 较高优先级
public class GlobalExceptionHandler2 {
    
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgument2(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("Global Handler 2: " + ex.getMessage());
    }
    
    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity<String> handleNullPointer(NullPointerException ex) {
        return ResponseEntity.badRequest().body("NullPointer handled by Handler2");
    }
}

// 控制器类
@RestController
public class MyController {
    
    @GetMapping("/test1")
    public String test1(@RequestParam(required = false) String param) {
        if (param == null) {
            throw new IllegalArgumentException("Param is required");
        }
        return "Success: " + param;
    }
    
    @GetMapping("/test2")
    public String test2() {
        throw new NullPointerException("Oops, null pointer");
    }
    
    // 本地异常处理 - 优先级最高
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleLocalIllegalArgument(IllegalArgumentException ex) {
        return ResponseEntity.badRequest().body("Local Controller Handler: " + ex.getMessage());
    }
}

测试结果分析

  1. 访问 /test1 (抛出 IllegalArgumentException):

    • 首先检查控制器本地的处理器 → 使用本地方法
    • 如果移除本地处理器 → 使用 GlobalHandler2(因为它的 @Order 值更小,优先级更高)
    • 如果 GlobalHandler2 也没有处理 → 使用 GlobalHandler1
  2. 访问 /test2 (抛出 NullPointerException):

    • 没有本地处理器 → 使用 GlobalHandler2 中的处理方法
    • 如果 GlobalHandler2 没有处理 → 查找其他 @ControllerAdvice 类中的处理器
    • 如果都没有 → 使用 Spring 默认异常处理

执行顺序总结

  1. 控制器本地@ExceptionHandler 方法(如果有)
  2. 按顺序 检查 @ControllerAdvice 类:
    • 先检查 @Order 值较小的类(数值越小优先级越高)
    • 在同一类中按方法声明顺序检查
  3. 最后是 Spring 的默认异常处理

最佳实践建议

  1. 合理组织异常处理器

    • 按业务领域或异常类型分类组织 @ControllerAdvice
    • 例如:ValidationExceptionAdvice, SecurityExceptionAdvice, BusinessExceptionAdvice
  2. 明确指定顺序

    java 复制代码
    @ControllerAdvice
    @Order(Ordered.HIGHEST_PRECEDENCE)  // 最高优先级
    public class SecurityExceptionAdvice {
        // 处理安全相关异常
    }
    
    @ControllerAdvice
    @Order(Ordered.LOWEST_PRECEDENCE)  // 最低优先级
    public class GenericExceptionAdvice {
        // 处理通用异常
    }
  3. 避免重复处理相同异常

    • 除非有明确需求,否则避免在多个 @ControllerAdvice 中定义相同的异常处理
    • 可以通过继承或组合来复用异常处理逻辑
  4. 记录未处理异常

    java 复制代码
    @ControllerAdvice
    @Order(Ordered.LOWEST_PRECEDENCE)
    public class LoggingExceptionAdvice {
        
        private static final Logger logger = LoggerFactory.getLogger(LoggingExceptionAdvice.class);
        
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ErrorResponse> handleUncaughtException(Exception ex) {
            logger.error("Unhandled exception occurred", ex);
            return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ErrorResponse("SERVER_ERROR", "An unexpected error occurred"));
        }
    }

通过合理利用多个 @ControllerAdvice 类和 @ExceptionHandler 方法,可以构建出清晰、灵活且易于维护的异常处理体系。

相关推荐
jieyucx2 分钟前
Go 零基础数据结构:顺序表(像「排抽屉」一样学增删改查)
java·数据结构·golang
曦夜日长3 分钟前
C++ STL容器string(一):string的变量细节、默认函数的认识以及常用接口的使用
java·开发语言·c++
WL_Aurora5 分钟前
MySQL 5 卸载到 MySQL 8 安装完整指南(不踩坑版)
数据库·mysql
灰阳阳7 分钟前
MySQL的基本架构
数据库·mysql·架构
北山有鸟10 分钟前
IS_ERR 判断出错后,再用 PTR_ERR 把它强制转换回 int 型的错误码作为函数的返回值。
java·开发语言
phltxy16 分钟前
深度解析:Spring Cloud Gateway 从入门到实战
java·开发语言
HAPPY酷20 分钟前
从Public到Private:UE5 C++类创建路径差异全解析
java·c++·ue5
@小柯555m26 分钟前
MySql(高级操作符--Where in 和Not in)
数据库·sql·mysql
许彰午27 分钟前
CacheSQL(一):手写数据库的工程化重生
java·数据库·缓存
MmeD UCIZ28 分钟前
MySQL单表存多大的数据量比较合适
数据库·mysql