@ControllerAdvice:全局异常处理

背景

在业务逻辑中存在大量重复的 try-catch 代码块(且没有全局异常处理机制)会带来以下显著问题:

  • 每个方法都需要手动编写 try-catch,导致大量重复代码,违反 DRY(Don’t Repeat Yourself)原则。
  • 如果异常处理逻辑需要调整(例如日志格式、错误码规范),必须逐个修改所有 try-catch 块,容易遗漏或出错。
public void processOrder() {
    try {
        // 业务逻辑
    } catch (OrderException e) {
        log.error("订单处理失败: {}", e.getMessage());
    }
}

public void refundOrder() {
    try {
        // 业务逻辑
    } catch (RefundException e) {
        log.error("退款失败: {}", e.getMessage());
    }
}

java中的异常处理方式

1、就地解决

即用try-catch块包裹住容易发生异常的代码片段,这样当该代码片段真正发生异常后,就会立即被catch块捕捉并在catch块中处理,从而使代码继续往下执行,不影响其他代码的执行。

2、向上抛出

如果不想立马就地处理,可以选择将该异常向上抛出,让方法的调用者处理。若方法的调用者也不行处理,同样可以继续向上抛出该异常,以此类推,直到将该异常抛给JVM处理。可是JVM懒啊,一看你们该处理的都不处理是吧,好,我也不处理,我还要把你们方法的调用过程给晒出来,结果就可以在控制台看到方法调用的堆栈信息了。

为什么要使用全局异常处理

总是用try-catch块包裹代码块也不好,影响性能不说代码看着不是很美观,所以我们选择用第二种——向上抛出的方式。向上抛出没问题,但总要有一个地方处理该异常,在web系统当中还应该将该异常以一个用户可以接受的方式返回给前端,不但在接口对接的时候让前端小姐姐知道是我们后台接口出现了问题,不至于摸不着头脑;而且我们后端开发人员也能根据接口返回的结果快速的知道到底是哪里出现了问题,才能快速解决问题。

全局异常处理

Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAttribute 等注解注解配套使用。

不过跟异常处理相关的只有注解@ExceptionHandler(该注解是springmvc中的一个注解)
该注解的作用:它通常用在控制器(Controller)类的方法上,以指定该方法用于处理特定类型的异常。当控制器中的其他方法抛出该异常时,Spring 会自动调用带有 @ExceptionHandler 注解的方法来处理该异常。

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

// 这是一个简单的控制器类
@RestController
public class MyController {

    // 一个可能会抛出异常的方法
    public String doSomething() {
        // 假设这里有一些逻辑,可能会抛出 IllegalArgumentException
        if (true) { // 这里只是为了演示,实际上应该有具体的逻辑判断
            throw new IllegalArgumentException("Invalid argument provided");
        }
        return "Success";
    }

    // 使用 @ExceptionHandler 来处理 IllegalArgumentException
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        // 创建一个包含错误信息的响应实体
        return new ResponseEntity<>("Invalid argument: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}
在上面的例子中,doSomething 方法可能会抛出一个 IllegalArgumentException。当这个异常被抛出时,Spring 会自动调用 handleIllegalArgumentException 方法来处理它,并返回一个包含错误信息的 ResponseEntity 对象,其 HTTP 状态码为 400 Bad Request

但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。这样一来,就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类了,很不优雅。

当然你可能会说,那就定义个类似BaseController的基类,这样总行了吧。

这种做法虽然没错,但仍不尽善尽美,因为这样的代码有一定的侵入性和耦合性。简简单单的Controller,我为啥非得继承这样一个类呢,万一已经继承其他基类了呢。大家都知道Java只能继承一个类。

那有没有一种方案,既不需要跟Controller耦合,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对 不同阶段的、不同异常 进行处理。这就是统一异常处理的原理。

@ControllerAdvice使用:

1、创建 MyControllerAdvice.java,如下:
package com.sam.demo.controller;

import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

/**
 * controller 增强器
 *
 * @author sam
 * @since 2017/7/17
 */
@ControllerAdvice
public class MyControllerAdvice {

    /**
     * 全局异常捕捉处理
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public Map errorHandler(Exception ex) {
        Map map = new HashMap();
        map.put("code", 100);
        map.put("msg", ex.getMessage());
        return map;
    }
    
    /**
     * 拦截捕捉自定义异常 MyException.class
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = MyException.class)
    public Map myErrorHandler(MyException ex) {
        Map map = new HashMap();
        map.put("code", ex.getCode());
        map.put("msg", ex.getMsg());
        return map;
    }

}
3、controller中抛出异常进行测试。
@RequestMapping("/home")
public String home() throws Exception {

//        throw new Exception("Sam 错误");
    throw new MyException("101", "Sam 错误");

}

启动应用,访问:http://localhost:8080/home ,正常显示以下json内容,证明自定义异常已经成功被拦截。

{"msg":"Sam 错误","code":"101"}

@ControllerAdvice的原理

1.组件扫描与注册

  • 注解标记:当类被 @ControllerAdvice 标注时,Spring 会在启动时通过组件扫描(Component Scan)发现该类。
  • Bean 注册:将该类注册为特殊的全局组件,其优先级高于普通 Controller,但低于具体的 Controller 方法。

2.异常处理流程
当 Controller 方法抛出异常时,Spring MVC 的处理流程如下:

  • 异常抛出:Controller 方法执行中抛出异常(如 throw new ServiceException())。
  • 异常传播:异常被 Spring 的 DispatcherServlet 捕获。
  • 查找处理器:Spring 遍历所有 @ControllerAdvice 类中的 @ExceptionHandler 方法,寻找与异常类型匹配的方法。
    执行处理:调用匹配的 @ExceptionHandler 方法生成响应(如返回 JSON 错误信息)。
  • 返回响应:将处理结果返回客户端,中断原始请求流程。
@ControllerAdvice
public class GlobalExceptionHandler {

    // 处理特定异常
    @ExceptionHandler(ServiceException.class)
    @ResponseBody
    public ResponseResult<Void> handleServiceException(ServiceException e) {
        return ResponseResult.fail(e.getCode(), e.getMessage());
    }

    // 处理所有未捕获的异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult<Void> handleException(Exception e) {
        return ResponseResult.fail(500, "系统繁忙");
    }
}

3.优先级规则

  • 精确匹配优先:@ExceptionHandler 方法定义的异常类型越具体,优先级越高。
  • Controller 内优先:若 Controller 内部定义了 @ExceptionHandler,则优先于全局 @ControllerAdvice。
  • 多个 @ControllerAdvice 类:通过 @Order 注解指定优先级,数值越小优先级越高。

你可能感兴趣的:(Spring专栏,异常,springmvc)