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 方法,可以构建出清晰、灵活且易于维护的异常处理体系。

相关推荐
岁忧1 分钟前
(nice!!!)(LeetCode 每日一题) 2561. 重排水果 (哈希表 + 贪心)
java·c++·算法·leetcode·go·散列表
阿华的代码王国14 分钟前
【Android】RecyclerView实现新闻列表布局(1)适配器使用相关问题
android·xml·java·前端·后端
码农BookSea18 分钟前
自研 DSL 神器:万字拆解 ANTLR 4 核心原理与高级应用
java·后端
lovebugs21 分钟前
Java并发编程:深入理解volatile与指令重排
java·后端·面试
慕y27426 分钟前
Java学习第九十一部分——OkHttp
java·开发语言·学习
caisexi27 分钟前
Windows批量启动java服务bat脚本
java·windows·python
NullPointerExpection34 分钟前
win10 环境删除文件提示文件被使用无法删除怎么办?
java·ide·windows·win10
Techie峰39 分钟前
【Java23种设计模式】:模板方法模式
java·设计模式·模板方法模式
llm44 分钟前
MediaPlayer介绍
java·架构