Spring 异常处理器:从混乱到有序,优雅处理所有异常
Spring 异常处理器:从混乱到有序,优雅处理所有异常
在 Java 开发中,异常处理是绕不开的话题。如果每个接口都用 try-catch 处理异常,不仅代码冗余,还可能导致异常处理逻辑分散、不一致(比如有的返回 500,有的返回自定义消息)。Spring 提供了一套强大的异常处理机制,能帮我们集中管理所有异常,让代码更简洁,异常响应更规范。
这篇文章从 “痛点分析” 到 “实战落地”,带你掌握 Spring 异常处理器的核心用法,让异常处理从 “混乱不堪” 变得 “井然有序”。
一、先看痛点:传统异常处理的问题
在没有统一异常处理时,我们通常会这样写代码:
@RestController
public class UserController {@Autowiredprivate UserService userService;@GetMapping("/user/{id}")public User getUserById(@PathVariable Long id) {try {// 业务逻辑return userService.getById(id);} catch (NullPointerException e) {// 处理空指针异常throw new RuntimeException("用户不存在");} catch (IllegalArgumentException e) {// 处理参数异常throw new RuntimeException("参数错误:" + e.getMessage());} catch (Exception e) {// 处理其他异常throw new RuntimeException("服务器出错了");}}
}
这种方式的问题很明显:
-
代码冗余:每个接口都要写重复的 try-catch;
-
风格不统一:不同开发者可能返回不同格式的错误信息;
-
维护困难:需要修改异常处理逻辑时,要改所有接口;
-
无法全局捕获:比如拦截器、过滤器中的异常可能漏处理。
二、Spring 异常处理的核心方案:@ControllerAdvice + @ExceptionHandler
Spring 提供了 全局异常处理器 机制,通过 @ControllerAdvice(控制器增强)和 @ExceptionHandler(异常处理器)注解,能将所有异常处理逻辑集中到一个类中,彻底解决上述问题。
核心原理:
-
@ControllerAdvice:标记一个类为 “全局异常处理类”,Spring 会自动扫描并生效;
-
@ExceptionHandler(异常类型.class):标记方法为 “特定异常的处理器”,当系统抛出该类型异常时,会自动调用该方法处理。
三、实战:实现全局异常处理器
以 “前后端分离项目” 为例,实现一个全局异常处理器,统一返回 JSON 格式的错误信息(包含状态码、错误消息、时间戳)。
步骤 1:定义统一的异常响应格式
先创建一个 “异常响应 DTO”,规范返回给前端的错误信息格式:
import lombok.Data;import java.time.LocalDateTime;// 统一异常响应格式
@Data
public class ErrorResponse {private Integer code; // 状态码(如 400、500)private String message; // 错误消息private LocalDateTime timestamp; // 发生时间public ErrorResponse(Integer code, String message) {this.code = code;this.message = message;this.timestamp = LocalDateTime.now();}
}
步骤 2:创建全局异常处理器类
用 @ControllerAdvice 和 @ExceptionHandler 实现全局异常处理:
import org.springframework.http.HttpStatus;
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.ResponseStatus;// 全局异常处理器(会处理所有 @Controller 标注的类的异常)
@ControllerAdvice
public class GlobalExceptionHandler {// 1. 处理自定义业务异常(最常用)@ExceptionHandler(BusinessException.class) // 指定处理 BusinessException 类型的异常@ResponseBody // 返回 JSON 格式public ErrorResponse handleBusinessException(BusinessException e) {// 返回自定义状态码和异常消息return new ErrorResponse(400, e.getMessage());}// 2. 处理空指针异常(系统异常示例)@ExceptionHandler(NullPointerException.class)@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 指定 HTTP 状态码为 500public ErrorResponse handleNullPointerException(NullPointerException e) {// 生产环境中不建议返回具体异常信息,避免泄露细节return new ErrorResponse(500, "服务器内部错误:空指针异常");}// 3. 处理参数绑定异常(如请求参数格式错误)@ExceptionHandler(IllegalArgumentException.class)@ResponseBodypublic ErrorResponse handleIllegalArgumentException(IllegalArgumentException e) {return new ErrorResponse(400, "参数错误:" + e.getMessage());}// 4. 处理所有未捕获的异常(兜底处理)@ExceptionHandler(Exception.class) // 父类异常,会捕获所有未被上面方法处理的异常@ResponseBody@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public ErrorResponse handleAllUncaughtException(Exception e) {return new ErrorResponse(500, "服务器繁忙,请稍后再试");}
}
步骤 3:定义自定义业务异常(可选但推荐)
实际开发中,业务逻辑异常(如 “用户已存在”“余额不足”)建议用自定义异常,更便于分类处理:
// 自定义业务异常
public class BusinessException extends RuntimeException {// 构造方法,接收错误消息public BusinessException(String message) {super(message);}
}
步骤 4:在业务中抛出异常
在 Service 或 Controller 中直接抛出异常,无需手动 try-catch,全局异常处理器会自动捕获并处理:
@Service
public class UserService {public User getById(Long id) {if (id == null || id <= 0) {// 抛出自定义业务异常(会被 handleBusinessException 处理)throw new BusinessException("用户ID必须大于0");}// 模拟查询用户(若查询结果为 null,会抛出空指针异常)User user = userMapper.selectById(id);if (user == null) {throw new BusinessException("用户不存在,ID:" + id);}return user;}
}@RestController
public class UserController {@Autowiredprivate UserService userService;// 接口中无需 try-catch,直接调用业务方法@GetMapping("/user/{id}")public User getUserById(@PathVariable Long id) {// 抛出的异常会被 GlobalExceptionHandler 自动处理return userService.getById(id);}
}
测试效果:
-
访问 /user/-1 → 触发 BusinessException → 返回 {“code”:400,“message”:“用户ID必须大于0”,“timestamp”:“xxx”};
-
访问 /user/999(不存在的 ID) → 触发 BusinessException → 返回 {“code”:400,“message”:“用户不存在,ID:999”,“timestamp”:“xxx”};
-
若 Service 中出现 null.getXXX() → 触发 NullPointerException → 返回 {“code”:500,“message”:“服务器内部错误:空指针异常”,“timestamp”:“xxx”}。
四、不同场景的适配:传统项目与前后端分离
全局异常处理器可根据项目类型(前后端分离 / 传统 JSP)返回不同结果:
1. 前后端分离(返回 JSON)
如上面的示例,用 @ResponseBody 直接返回 ErrorResponse 对象(自动序列化为 JSON)。
2. 传统项目(返回错误页面)
若项目用 JSP/Thymeleaf 渲染页面,可返回错误视图名:
@ControllerAdvice
public class GlobalExceptionHandler {// 处理业务异常,返回错误页面@ExceptionHandler(BusinessException.class)public ModelAndView handleBusinessException(BusinessException e) {ModelAndView mv = new ModelAndView();mv.addObject("errorMsg", e.getMessage()); // 错误消息mv.setViewName("error"); // 错误页面(如 error.jsp)return mv;}
}
五、异常处理的优先级:精确匹配优先
当多个异常处理器都能处理某个异常时(如子类异常和父类异常),Spring 会遵循 “精确匹配优先” 原则:
-
先执行最匹配的异常处理器(子类异常处理器);
-
若没有,则执行父类异常处理器。
示例:
-
抛出 NullPointerException 时,会优先执行 @ExceptionHandler(NullPointerException.class) 标注的方法;
-
若没有专门处理 NullPointerException 的方法,才会执行 @ExceptionHandler(Exception.class) 标注的兜底方法。
六、避坑指南:这些错误别犯
1. 异常处理器未生效?检查这 3 点
-
确保类上标注了 @ControllerAdvice;
-
确保异常处理器方法上标注了 @ExceptionHandler 并指定了正确的异常类型;
-
确保 Spring 能扫描到异常处理器类(在 @ComponentScan 的扫描路径内)。
2. 不要在异常处理器中 “吞掉异常”
错误示例:
@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {// 错误:没有记录异常堆栈,难以排查问题return new ErrorResponse(500, "服务器错误");
}
正确做法:记录异常堆栈(至少在开发环境):
@ExceptionHandler(Exception.class)
public ErrorResponse handleException(Exception e) {// 记录异常详情(日志框架如 Logback/Log4j)log.error("未捕获异常", e); // 关键:打印堆栈信息return new ErrorResponse(500, "服务器繁忙,请稍后再试");
}
3. 自定义异常建议继承 RuntimeException
Spring 事务默认只对 RuntimeException 及其子类回滚。若自定义异常继承 Exception(受检异常),需手动配置 @Transactional(rollbackFor = 自定义异常.class) 才会回滚事务,增加复杂度。
七、总结:全局异常处理器的核心价值
-
代码简洁:消除重复的 try-catch,控制器和服务层只需专注业务逻辑;
-
统一规范:所有异常返回格式一致(状态码、消息结构),前端处理更简单;
-
易于维护:异常处理逻辑集中在一个类,修改时只需改一处;
-
覆盖全面:能捕获控制器、服务层、甚至拦截器中的异常(过滤器中的异常需额外处理)。
掌握 @ControllerAdvice + @ExceptionHandler 是 Spring 开发的必备技能,无论是小型项目还是大型企业应用,这套机制都能显著提升异常处理的效率和规范性。实际开发中,建议结合自定义业务异常,让异常分类更清晰,处理更精准。