开篇:先看一个痛点 —— 异常返回的 “混乱现场”
后端接口返回数据时,我们通常希望格式统一,比如这样:

  • 成功{"flag":true, "code":200, "message":"操作成功", "data":{"id":1,"name":"张三"}}
  • 业务失败(比如参数错误): {"flag":false, "code":400, "message":"手机号格式错误", "data":null}

但如果代码抛异常(比如空指针、数据库报错),SpringBoot 会返回默认的错误格式:

{
  "timestamp":"2024-05-20T03:27:31.038+00:00",
  "status":500,
  "error":"Internal Server Error",
  "path":"/books"
}

前端拿到两种完全不同的格式,得写两套逻辑处理,很麻烦。这就是异常处理不统一的问题。

一、两种解决方案:try-catch vs 全局异常处理

怎么让异常返回也统一格式?有两种方案,对比之后高下立判。

方案 1:try-catch 手动捕获(不推荐)

在每个 Controller 的方法里加try-catch,捕获异常后返回统一格式:

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService;

    @GetMapping("/{id}")
    public Result getBook(@PathVariable Integer id) {
        try {
            // 业务逻辑
            Book book = bookService.getById(id);
            return Result.success(book); // 成功返回统一格式
        } catch (Exception e) {
            // 捕获异常,返回统一格式
            e.printStackTrace();
            return Result.error("操作失败,请联系管理员");
        }
    }

    // 其他方法(add、update、delete)也要加try-catch...
}

缺点:

  • 代码臃肿:每个接口都要写重复的try-catch,占大量代码;
  • 维护麻烦:要改错误提示,得改所有catch块;
  • 容易遗漏:新增接口忘了加try-catch,异常返回又乱了。

方案 2:全局异常处理(推荐,优雅又省心)

SpringBoot 提供了 “全局异常处理器”——用一个类捕获所有 Controller 抛出的异常,不用在每个接口写try-catch。核心是两个注解:@RestControllerAdvice(全局捕获控制器异常)和@ExceptionHandler(指定处理哪种异常)。
为什么推荐?

  • 代码简洁:一个处理器搞定所有异常;
  • 统一维护:错误格式、提示信息集中管理;
  • 无遗漏:所有 Controller 的异常都会被捕获。

二、基础实战:3 步实现全局异常处理

步骤 1:定义统一返回结果类(Result)

首先要有一个标准化的返回类,确保成功、失败、异常的返回格式一致。

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一返回结果类
 */
@Data  // Lombok注解,自动生成getter/setter
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean flag;  // 成功标识:true=成功,false=失败
    private Integer code;  // 状态码:200=成功,400=参数错误,500=系统异常
    private String message; // 提示信息
    private Object data;   // 返回数据(成功时返回,失败时为null)

    // 1. 成功返回(带数据)
    public static Result success(Object data) {
        return new Result(true, 200, "操作成功", data);
    }

    // 2. 成功返回(无数据)
    public static Result success() {
        return new Result(true, 200, "操作成功", null);
    }

    // 3. 失败返回(自定义提示)
    public static Result error(String message) {
        return new Result(false, 500, message, null);
    }

    // 4. 失败返回(自定义状态码和提示)
    public static Result error(Integer code, String message) {
        return new Result(false, code, message, null);
    }
}

步骤 2:写全局异常处理器(核心)

创建一个处理器类,用@RestControllerAdvice标记,再用@ExceptionHandler写异常处理方法。

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器
 */
// @RestControllerAdvice = @ControllerAdvice + @ResponseBody(返回JSON格式)
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理所有Exception类型的异常(兜底处理)
     * @ExceptionHandler(Exception.class):指定处理Exception及其子类异常
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        // 1. 打印异常栈信息(方便开发排查问题,生产环境可用日志框架记录)
        e.printStackTrace();
        // 2. 返回统一失败格式
        return Result.error("服务器故障,请稍后重试");
    }
}

步骤 3:测试效果(异常被统一捕获)

写一个会抛异常的接口,测试处理器是否生效:

@RestController
@RequestMapping("/books")
public class BookController {

    @GetMapping("/error")
    public Result testError() {
        // 故意抛一个空指针异常
        String str = null;
        str.length(); // 这里会抛NullPointerException
        return Result.success();
    }
}

访问接口:
启动项目,访问http://localhost:8080/books/error,返回结果:

{"flag":false,"code":500,"message":"服务器故障,请稍后重试","data":null}

格式统一了!不再是 SpringBoot 默认的错误格式。

三、进阶实战:分类型处理异常(返回更精准的提示)

基础版把所有异常都返回 “服务器故障”,但实际开发中,不同异常要返回不同提示(比如 “参数错误”“数据不存在”)。
解决方法自定义异常类,再在处理器中针对不同异常写不同的处理方法。

步骤 1:自定义业务异常类(BusinessException)

系统异常(如空指针)和业务异常(如 “手机号已存在”)要区分,自定义异常用于标记业务异常:

/**
 * 自定义业务异常(比如参数错误、数据不存在等)
 */
public class BusinessException extends RuntimeException {
    // 状态码(比如400=参数错误,404=数据不存在)
    private Integer code;

    // 构造方法(传入状态码和提示信息)
    public BusinessException(Integer code, String message) {
        super(message); // 调用父类的构造方法,保存提示信息
        this.code = code;
    }

    // getter方法(获取状态码)
    public Integer getCode() {
        return code;
    }
}

步骤 2:在处理器中添加 “业务异常处理方法”

一个处理器可以有多个@ExceptionHandler方法,Spring 会自动匹配 “最具体的异常类型”(比如抛BusinessException,优先走对应的处理方法,而不是兜底的Exception方法)。
更新后的处理器:

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 1. 处理业务异常(自定义异常)
     */
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        e.printStackTrace();
        // 返回自定义的状态码和提示(比如400+“参数错误”)
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 2. 处理系统异常(兜底,比如NullPointerException、SQL异常等)
     */
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        e.printStackTrace();
        return Result.error("服务器故障,请稍后重试");
    }
}

步骤 3:在业务中抛自定义异常

比如在 Service 层判断 “手机号已存在”,抛BusinessException:

@Service
public class UserService {

    // 模拟数据库中已存在的手机号
    private static final String EXIST_PHONE = "13800138000";

    public void register(String phone) {
        // 业务判断:如果手机号已存在,抛业务异常
        if (EXIST_PHONE.equals(phone)) {
            // 状态码400=参数错误,提示“手机号已注册”
            throw new BusinessException(400, "手机号已注册");
        }
        // 正常注册逻辑...
    }
}

步骤 4:测试业务异常

写一个注册接口测试:

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

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public Result register(@RequestParam String phone) {
        userService.register(phone);
        return Result.success("注册成功");
    }
}

访问接口:
请求http://localhost:8080/users/register?phone=13800138000,返回结果:

{"flag":false,"code":400,"message":"手机号已注册","data":null}

提示精准了!不再是模糊的 “服务器故障”。

四、关键注解与原理:小白必须懂

1. @RestControllerAdvice 注解

作用:标记这是一个 “全局控制器异常处理器”,会捕获所有@Controller/@RestController抛出的异常;
本质@ControllerAdvice + @ResponseBody——@ControllerAdvice负责全局捕获,@ResponseBody确保返回 JSON 格式
扫描范围:默认扫描当前包及子包的控制器,也可以用basePackages指定范围(比如@RestControllerAdvice(basePackages = "com.heima.controller"))

2. @ExceptionHandler 注解

  • 作用:标记方法为 “异常处理方法”,只能用在@RestControllerAdvice@ControllerAdvice修饰的类中;
  • 参数:必须指定 “处理的异常类型”(比如Exception.class、BusinessException.class);
  • 匹配规则:Spring 会优先选择 “最具体的异常类型” 对应的方法(比如NullPointerException会先找@ExceptionHandler(NullPointerException.class),没有再找@ExceptionHandler(Exception.class))。

五、小白避坑指南:3 个常见问题

1. 异常处理器不生效?

  • 原因 1:处理器类没被 Spring 扫描到(比如处理器在com.heima.exception包,引导类在com.heima包,默认会扫描子包,没问题;但如果处理器在com.exception包,就扫描不到);
  • 解决:要么把处理器移到引导类的子包下,要么在引导类加@ComponentScan("com.exception")
  • 原因 2:注解导错包 ——@RestControllerAdvice是org.springframework.web.bind.annotation.RestControllerAdvice,别导成其他包。

2. 异常处理方法没拿到异常对象?

  • 原因:方法参数没加 “异常类型”—— 异常处理方法必须包含 “要处理的异常对象” 作为参数;
  • 错误示例:public Result handleException() { … }(没有异常参数,无法捕获);
  • 正确示例:public Result handleException(Exception e) { … }(有 Exception 参数)。

3. 生产环境能打印 e.printStackTrace () 吗?

  • 不推荐:e.printStackTrace()会把栈信息打印到控制台,生产环境不好查看,且性能一般;
  • 推荐:用日志框架(如 SLF4J+Logback)记录异常,比如:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestControllerAdvice
public class GlobalExceptionHandler {
    // 定义日志对象
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        // 用日志记录异常
        log.error("业务异常:", e); // 第二个参数e要传,才会打印栈信息
        return Result.error(e.getCode(), e.getMessage());
    }
}

六、总结:全局异常处理核心要点

1. 核心目标:统一异常返回格式,减少重复代码;
2. 核心组件

  • Result类:标准化返回格式(flag+code+message+data);
  • @RestControllerAdvice:全局捕获控制器异常;
  • @ExceptionHandler:分类型处理异常;

3. 进阶技巧

  • 自定义BusinessException区分业务异常系统异常
  • 用日志框架记录异常,方便排查问题;

4. 避坑关键:确保处理器被 Spring 扫描,方法参数包含异常对象。

全局异常处理是 SpringBoot 开发的 “标配技能”,花 10 分钟实现,能让你的接口更规范、维护更省心,小白赶紧动手试试吧!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐