SpringBoot 异常处理实战:从 “格式混乱” 到 “全局统一方案”
开篇:先看一个痛点 —— 异常返回的 “混乱现场”
后端接口返回数据时,我们通常希望格式统一,比如这样:
- 成功:
{"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 分钟实现,能让你的接口更规范、维护更省心,小白赶紧动手试试吧!
更多推荐
所有评论(0)