当前位置: 首页 > news >正文

Spring Boot自定义全局异常处理:从痛点到优雅实现

在 Spring Boot 项目开发中,异常处理是保障系统稳定性的关键环节。传统的try-catch分散在各个 Controller、Service 中,不仅导致代码冗余,还会出现同一异常不同响应格式的问题 —— 比如有的接口返回{"code":500,"msg":"错误"},有的直接返回 Tomcat 默认的 404 页面,前端处理时需反复适配。

自定义全局异常处理能统一接管项目中所有异常,实现 “一处定义、全局生效”,让异常响应格式标准化、代码逻辑更简洁。本文将从核心原理到实战落地,带你掌握 Spring Boot 全局异常处理的完整方案。

一、为什么需要全局异常处理?先看传统方案的痛点

在没有全局异常处理时,我们通常这样处理异常:
@RestController
@RequestMapping("/user")
public class UserController {@GetMapping("/{id}")public String getUserById(@PathVariable Long id) {try {if (id == null || id <= 0) {throw new IllegalArgumentException("用户ID非法");}User user = userService.getById(id);if (user == null) {return "{\"code\":404,\"msg\":\"用户不存在\",\"data\":null}";}return "{\"code\":200,\"msg\":\"成功\",\"data\":" + JSON.toJSONString(user) + "}";} catch (IllegalArgumentException e) {return "{\"code\":400,\"msg\":\"" + e.getMessage() + "\",\"data\":null}";} catch (Exception e) {// 系统异常,隐藏具体信息return "{\"code\":500,\"msg\":\"服务器内部错误\",\"data\":null}";}}
}

这种方式存在 3 个明显问题:

  1. 代码冗余:每个接口都要写重复的try-catch和响应格式拼接;
  2. 格式不统一:若多个开发者定义不同的codemsg字段,前端需反复适配;
  3. 异常遗漏:若忘记捕获某个异常(如NullPointerException),会返回默认 500 错误页,体验极差。

而全局异常处理能一次性解决这些问题 —— 只需定义一套规则,所有异常都会按统一格式返回,且无需在业务代码中写try-catch

二、核心技术:Spring Boot 的异常处理注解

Spring Boot 通过 3 个核心注解实现全局异常处理,需先理解其作用:
注解作用说明
@RestControllerAdvice全局异常处理的 “入口”,是@ControllerAdvice+@ResponseBody的组合,自动返回 JSON 格式响应(适合前后端分离);若用@ControllerAdvice,需配合@ResponseBody使用。
@ExceptionHandler定义 “异常处理器方法”,指定该方法处理哪类异常(如@ExceptionHandler(NullPointerException.class)处理空指针异常)。
@ResponseStatus可选,为异常响应设置 HTTP 状态码(如 400、404、500),默认返回 200 OK。

此外,还需用到统一响应类—— 封装所有接口(正常响应 + 异常响应)的返回格式,确保前后端交互字段一致。

三、实战:从零实现全局异常处理

下面按 “统一响应→自定义异常→全局处理器→测试验证” 的步骤,搭建完整的全局异常处理方案。

步骤 1:定义统一响应类(核心前提)

首先创建`Result`类,统一所有接口的返回格式,包含 3 个核心字段:
  • code:业务状态码(如 200 = 成功,400 = 参数错误,500 = 系统错误);
  • msg:响应消息(成功 / 错误描述);
  • data:响应数据(正常响应时返回业务数据,异常时为null)。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 统一响应类*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {// 业务状态码private Integer code;// 响应消息private String msg;// 响应数据private T data;// -------------- 静态工厂方法:简化调用 --------------// 成功(无数据)public static <T> Result<T> success() {return new Result<>(200, "操作成功", null);}// 成功(有数据)public static <T> Result<T> success(T data) {return new Result<>(200, "操作成功", data);}// 失败(自定义状态码和消息)public static <T> Result<T> fail(Integer code, String msg) {return new Result<>(code, msg, null);}// 失败(默认系统错误)public static <T> Result<T> error() {return new Result<>(500, "服务器内部错误", null);}
}

有了这个类,正常接口的返回会更简洁,例如:

// 正常响应示例
@GetMapping("/{id}")
public Result<User> getUserById(@PathVariable Long id) {User user = userService.getById(id);if (user == null) {// 直接返回失败响应,无需拼接JSONreturn Result.fail(404, "用户不存在");}return Result.success(user);
}

步骤 2:定义自定义业务异常

实际开发中,很多异常是业务相关的(如 “用户余额不足”“订单已取消”),需要携带业务状态码。此时需定义**自定义异常类**,继承`RuntimeException`(无需强制捕获),并包含`code`字段。
import lombok.Getter;/*** 自定义业务异常(如用户不存在、参数非法等)*/
@Getter // 提供code和message的getter方法,供全局处理器获取
public class BusinessException extends RuntimeException {// 业务状态码(如404=用户不存在,400=参数错误)private final Integer code;// 构造方法1:自定义code和messagepublic BusinessException(Integer code, String message) {super(message); // 父类RuntimeException的message字段this.code = code;}// 构造方法2:默认code=400(参数错误)public BusinessException(String message) {this(400, message);}
}

后续业务中抛出异常时,直接用自定义异常:

// 业务层抛出自定义异常示例
public void deductBalance(Long userId, BigDecimal amount) {User user = userMapper.selectById(userId);if (user == null) {// 抛出“用户不存在”异常,携带code=404throw new BusinessException(404, "用户不存在");}if (user.getBalance().compareTo(amount) < 0) {// 抛出“余额不足”异常,默认code=400throw new BusinessException("用户余额不足");}// 后续扣减余额逻辑...
}

步骤 3:编写全局异常处理器(核心实现)

创建GlobalExceptionHandler类,用@RestControllerAdvice标注,内部定义多个@ExceptionHandler方法,分别处理不同类型的异常(自定义异常、系统异常、参数校验异常等)。

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全局异常处理器:统一处理所有异常*/
@RestControllerAdvice(basePackages = "cn.varin.demo.controller") // 只处理指定包下的Controller异常
@Slf4j // 记录异常日志
public class GlobalExceptionHandler {// ---------------- 1. 处理自定义业务异常 ----------------@ExceptionHandler(BusinessException.class)public Result<Void> handleBusinessException(BusinessException e) {// 记录异常日志(级别为WARN,因为是业务已知异常)log.warn("业务异常:{},状态码:{}", e.getMessage(), e.getCode());// 返回自定义的业务状态码和消息return Result.fail(e.getCode(), e.getMessage());}// ---------------- 2. 处理参数校验异常(如@NotNull、@Size) ----------------// 注:需配合Spring Validation依赖(如spring-boot-starter-validation)@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST) // 设置HTTP状态码为400public Result<Void> handleValidException(MethodArgumentNotValidException e) {// 获取参数校验失败的第一个错误信息String errMsg = e.getBindingResult().getFieldError().getDefaultMessage();log.warn("参数校验异常:{}", errMsg);// 返回code=400和错误信息return Result.fail(400, errMsg);}// ---------------- 3. 处理系统通用异常(如NullPointerException、IllegalArgumentException) ----------------@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置HTTP状态码为500public Result<Void> handleGeneralException(Exception e) {// 记录异常堆栈信息(级别为ERROR,因为是未知系统异常,需排查)log.error("系统异常:", e); // 注意这里要传e,才能打印堆栈// 隐藏具体异常信息,返回通用提示(避免泄露系统细节)return Result.error();}
}
关键说明:
  1. basePackages属性:限定处理器的作用范围,只处理cn.varin.demo.controller包下的 Controller 抛出的异常,避免处理其他第三方组件的异常;
  2. 日志记录分级:业务异常(BusinessException)用warn级别(已知问题,无需紧急处理),系统异常用error级别(未知问题,需排查);
  3. @ResponseStatus:为异常响应设置 HTTP 状态码(如参数校验失败返回 400,系统错误返回 500),前端可通过状态码快速判断异常类型;
  4. 参数校验异常:需引入spring-boot-starter-validation依赖,配合@Valid注解使用(后续会演示)。

步骤 4:引入参数校验依赖(可选但推荐)

若要处理参数校验异常(如@NotNull@Min),需在pom.xml中引入依赖:

<!-- Spring Validation:参数校验 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

步骤 5:测试验证

创建测试 Controller,模拟不同异常场景,验证全局异常处理是否生效。

测试 1:自定义业务异常
@RestController
@RequestMapping("/user")
public class UserController {@PostMapping("/deduct")public Result<Void> deductBalance(@RequestParam Long userId, @RequestParam BigDecimal amount) {// 调用业务层方法,内部会抛出BusinessExceptionuserService.deductBalance(userId, amount);return Result.success();}
}

请求示例POST /user/deduct?userId=999&amount=100(用户 ID=999 不存在)

响应结果(统一格式):

{"code": 404,"msg": "用户不存在","data": null}

控制台日志WARN cn.varin.demo.exception.GlobalExceptionHandler - 业务异常:用户不存在,状态码:404

测试 2:参数校验异常
// 用@Valid注解开启参数校验,@RequestBody接收对象参数
@PostMapping("/add")
public Result<User> addUser(@Valid @RequestBody User user) {userService.save(user);return Result.success(user);
}// User类(添加参数校验注解)
@Data
public class User {@NotNull(message = "用户名不能为空") // 校验username不能为null@Size(min = 2, max = 10, message = "用户名长度必须在2-10之间") // 校验长度private String username;@Min(value = 18, message = "年龄不能小于18岁") // 校验age最小值private Integer age;
}

请求示例:提交{"username":"a","age":17}(用户名长度 1,年龄 17)

响应结果

{"code": 400,"msg": "用户名长度必须在2-10之间","data": null}

控制台日志WARN cn.varin.demo.exception.GlobalExceptionHandler - 参数校验异常:用户名长度必须在2-10之间

测试 3:系统异常(空指针)
@GetMapping("/test/error")
public Result<Void> testSystemError() {// 模拟空指针异常(未处理)String str = null;str.length(); // 此处会抛出NullPointerExceptionreturn Result.success();
}

请求示例GET /user/test/error

响应结果(隐藏具体异常信息):

{"code": 500,"msg": "服务器内部错误","data": null}

控制台日志(打印完整堆栈,方便排查):

ERROR cn.varin.demo.exception.GlobalExceptionHandler - 系统异常:
java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
at cn.varin.demo.controller.UserController.testSystemError(UserController.java:45)
...(后续堆栈省略)

四、进阶场景:处理特殊异常

除了上述常见场景,还有一些特殊异常需要单独处理,确保覆盖全面。

1. 处理 404(资源未找到)异常

Spring Boot 默认的 404 响应是 HTML 页面,需自定义 404 异常处理器,返回 JSON 格式:

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) // HTTP状态码404
public Result<Void> handle404Exception(NoHandlerFoundException e) {
log.warn("资源未找到:{}", e.getRequestURL());
return Result.fail(404, "请求的接口不存在");
}

注意:需在application.yml中开启 404 异常抛出配置,否则无法被全局处理器捕获:

spring:mvc:throw-exception-if-no-handler-found: true # 开启404异常抛出web:resources:add-mappings: false # 禁用默认的静态资源映射(避免静态资源404被捕获)

2. 处理异步方法中的异常

若方法用@Async标注(异步执行),其抛出的异常无法被默认的全局处理器捕获,需自定义AsyncUncaughtExceptionHandler

import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;@Configuration
@EnableAsync // 开启异步支持
public class AsyncConfig implements AsyncConfigurer {// 自定义异步异常处理器@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return (ex, method, params) -> {// 记录异步异常日志log.error("异步方法[{}]执行异常,参数:{}", method.getName(), params, ex);// 若需要通知(如邮件、钉钉),可在此处添加逻辑};}
}

3. 区分开发 / 生产环境的异常响应

开发环境需要显示异常堆栈(方便调试),生产环境需隐藏细节。可通过@Profile注解实现环境区分:

// 开发环境:返回详细异常信息
@ExceptionHandler(Exception.class)
@Profile("dev") // 仅dev环境生效
public Result<Void> handleDevGeneralException(Exception e) {
log.error("系统异常:", e);
// 返回异常堆栈的字符串形式(简化)
String stackTrace = Arrays.stream(e.getStackTrace())
.map(StackTraceElement::toString)
.limit(5) // 只显示前5行堆栈
.collect(Collectors.joining("\n"));
return Result.fail(500, "开发环境异常:" + e.getMessage() + "\n" + stackTrace);
}// 生产环境:隐藏细节
@ExceptionHandler(Exception.class)
@Profile("prod") // 仅prod环境生效
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleProdGeneralException(Exception e) {
log.error("系统异常:", e);
return Result.fail(500, "服务器繁忙,请稍后再试");
}

五、常见问题与解决方案

  1. 全局异常处理器不生效?
  • 检查@RestControllerAdvicebasePackages是否包含 Controller 所在包;
  • 检查异常是否被业务代码中的try-catch捕获(若捕获后未重新抛出,处理器无法感知);
  • 检查是否引入了正确的依赖(如参数校验需spring-boot-starter-validation)。
  1. 404 异常无法被捕获?
  • 必须在配置文件中开启spring.mvc.throw-exception-if-no-handler-found=truespring.web.resources.add-mappings=false
  1. 异步方法异常无法被捕获?
  • 需实现AsyncConfigurer,自定义AsyncUncaughtExceptionHandler,不能依赖默认的全局处理器。

六、最佳实践总结

  1. 统一响应格式:所有接口(正常 / 异常)都通过Result类返回,避免格式混乱;
  2. 分类处理异常:自定义业务异常(已知)、系统异常(未知)、特殊异常(404、异步)分开处理,日志分级记录;
  3. 隐藏敏感信息:生产环境不返回异常堆栈,避免泄露系统架构;
  4. 配合参数校验:用Spring Validation+ 全局处理器自动处理参数错误,减少重复代码;
  5. 环境差异化:开发环境返回详细异常信息,生产环境返回友好提示。

通过自定义全局异常处理,不仅能提升代码的简洁性和可维护性,还能让前后端交互更高效。你在项目中遇到过哪些特殊的异常处理场景?欢迎在评论区交流!

http://www.dtcms.com/a/428190.html

相关文章:

  • 网站正在建设中的素材动图网站设计制作程序
  • 企业网站的搭建流程河南论坛网站建设
  • wap网站建设策划方案做艺术字的网站
  • 电脑实用工具,资源下载
  • 曲阳有没有做网站里2345网站登录
  • PostgreSQL 向量操作符的计算和使用方式
  • 动态代理在提升网络安全中的作用及应用
  • 宁夏微信服务网站百度网盘资源搜索
  • 手机做网站用什么软件微信下载官方正版
  • Redis缓存异常
  • 建设网站iss局机关门户网站建设情况汇报
  • 做网站需要哪些东西163免费注册入口
  • 【Rust GUI开发入门】编写一个本地音乐播放器(9. 制作设置面板)
  • 概率统计中的数学语言与术语2
  • 美国2025年网络演习全景与趋势洞察
  • 公司做网站有什么用编程和做网站那个号
  • 做公司网站都需要什么免费广告设计网站
  • IO-link 协议高频工业 RFID 读写器
  • NeurIPS 2025 | 北大等提出C²Prompt:解耦类内与类间知识,攻克FCL遗忘难题!
  • 网站推广有哪些举措全屋定制品牌推荐
  • 1元建站wordpress短代码返回html
  • 极简学习工具产品蓝图、路线图、甘特图、交付清单
  • 为网站设计手机版深圳工业设计公司哪家好
  • 定制网站建设济南开发app需要哪些审批
  • 古老的游戏(游戏的娱乐属性)
  • 先进网站建设流程洛宁网站建设
  • 网站开发示例网页广告如何关闭
  • 免费的行情网站app软件大全母婴网站设计分析
  • 四川网站网页设计网站维护 北京
  • 手撕ArrayList,ArrayList底层原理是什么,它是怎么扩容的?