1. 为什么需要全局异常处理?
在没有统一异常处理的情况下,常见问题包括:
- 用户传入非法参数,后端直接抛出
NullPointerException,前端收到 500; - 数据库主键冲突,返回一大段 Java 堆栈,暴露系统内部结构;
- 不同 Controller 对相同异常(如“用户不存在”)返回不同格式,前端难以统一处理。
✅ 解决方案:使用 Spring 的 @ControllerAdvice + @ExceptionHandler 实现 全局异常拦截与标准化响应。
2. 结合 JsonResult,设计异常响应结构
我们复用上一课定义的 JsonResult<T>,并约定:
- 成功:
code = "0" - 失败:
code ≠ "0",msg描述原因,data = null
✅ 示例:用户未找到异常
{ "code": "1002", "msg": "用户不存在", "data": null }
3. 自定义业务异常类(可选但推荐)
为区分系统异常和业务异常,建议定义自己的异常:
public class BusinessException extends RuntimeException { private String code; public BusinessException(String code, String message) { super(message); this.code = code; } // getter public String getCode() { return code; } }
常见业务异常码示例:
"1001":参数校验失败"1002":资源不存在"1003":权限不足
4. 全局异常处理器:GlobalExceptionHandler
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice // = @ControllerAdvice + @ResponseBody public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 捕获自定义业务异常 */ @ExceptionHandler(BusinessException.class) public JsonResult<?> handleBusinessException(BusinessException e) { logger.warn("业务异常:code={}, msg={}", e.getCode(), e.getMessage()); return new JsonResult<>(e.getCode(), e.getMessage()); } /** * 捕获参数校验异常(如 @Valid 失败) */ @ExceptionHandler(MethodArgumentNotValidException.class) public JsonResult<?> handleValidationException(MethodArgumentNotValidException e) { String msg = e.getBindingResult().getFieldError().getDefaultMessage(); logger.warn("参数校验失败:{}", msg); return new JsonResult<>("1001", msg != null ? msg : "请求参数错误"); } /** * 捕获空指针等运行时异常(兜底) */ @ExceptionHandler(Exception.class) public JsonResult<?> handleGenericException(Exception e) { logger.error("系统异常:", e); // 记录完整堆栈 return new JsonResult<>("500", "系统繁忙,请稍后再试"); } }
🔑 关键点:
@RestControllerAdvice:对所有@RestController生效;- 优先匹配具体异常(如
BusinessException),最后用Exception兜底;- 务必记录日志,便于排查问题。
5. 在业务代码中主动抛出异常
@Service public class UserService { public User getUserById(Long id) { if (id == null || id <= 0) { throw new BusinessException("1001", "用户ID不能为空且必须大于0"); } User user = userMapper.selectById(id); if (user == null) { throw new BusinessException("1002", "用户不存在"); } return user; } }
Controller 层无需 try-catch:
@GetMapping("/user/{id}") public JsonResult<User> getUser(@PathVariable Long id) { User user = userService.getUserById(id); return new JsonResult<>(user); }
6. 测试效果
请求:GET /user/999(用户不存在)
{ "code": "1002", "msg": "用户不存在", "data": null }
请求:GET /user/0(参数非法)
{ "code": "1001", "msg": "用户ID不能为空且必须大于0", "data": null }
发生未知异常(如数据库宕机)
{ "code": "500", "msg": "系统繁忙,请稍后再试", "data": null }
✅ 所有响应格式统一,前端只需判断
code即可处理!
7. 进阶建议
| 场景 | 方案 |
| 国际化错误消息 | 将 msg 替换为 message key,前端根据语言包显示 |
| 记录异常到监控系统 | 在 handleGenericException 中集成 Sentry、SkyWalking 等 |
| 区分开发/生产环境 | 开发环境可返回 e.getMessage(),生产环境固定提示 |
| 配合 Validator | 使用 @Valid + BindingResult 或全局捕获 MethodArgumentNotValidException |
8. 总结
通过本篇学习,你已掌握:
- ✅ 为什么需要全局异常处理;
- ✅ 如何定义
BusinessException和JsonResult配合使用; - ✅ 使用
@RestControllerAdvice统一拦截异常; - ✅ 在业务层主动抛出语义化异常;
- ✅ 保证所有 API 响应格式一致、安全、友好。