首页| 论坛| 搜索| 消息
主题:Spring Boot 项目里 90% 的人都写错了全局异常处理
爱我中华发表于 2026-06-01 13:24
你的 @RestControllerAdvice 真的兜住了所有异常吗?线上告警满天飞,日志里全是堆栈,接口返回时而 JSON 时而 HTML——这些问题,说到底都是异常处理没做对。
1. 先看你现在的代码是不是这样
@RestControllerAdvicepublic class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public Result handleException(Exception e) {log.error("系统异常", e);return Result.fail("系统繁忙");}}
这代码初看没毛病,上线就跑偏——JSON 解析异常根本进不来。
Spring 在处理请求体反序列化时,HttpMessageNotReadableException 抛出时机早于 Controller 方法调用,默认的 /error 路径会给你返回一个 HTML 页面。前端直接裂开。
2. 先堵死默认的 /error 路径

Spring Boot 的 BasicErrorController 是万恶之源。你定义了 @RestControllerAdvice,但某些异常绕过它走了 /error,返回的就是 HTML。@RestControllerpublic class NotFoundController implements ErrorController {private static final String ERROR_PATH = "/error";@RequestMapping(ERROR_PATH)public Result handleError(HttpServletRequest request) {Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");if (statusCode == 404) {return Result.fail(404, "接口不存在");}return Result.fail(500, "服务器内部错误");}}
这个 Controller 要放在能被扫描到的包里,不然还是不生效。
3. 统一响应体——别再用 Map 了

Map 拼 JSON 是典型的"能跑就行"思维。字段名全靠手敲,拼错一个线上排查半小时。@Data@NoArgsConstructor@AllArgsConstructorpublic class Result {private int code;private String message;private T data;public static Result ok(T data) {return new Result(200, "success", data);}public static Result fail(String message) {return new Result(500, message, null);}public static Result fail(int code, String message) {return new Result(code, message, null);}}
泛型加上的好处:Swagger 能自动推断返回数据结构,接口文档不再是一团 Object。
4. 参数校验异常——被忽略的重灾区

这个坑踩过的人最多:参数上加了 @Valid,校验失败抛了MethodArgumentNotValidException,但你的全局异常处理里根本没接。@ExceptionHandler(MethodArgumentNotValidException.class)public Result handleValidException(MethodArgumentNotValidException e) {String msg = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining("; "));return Result.fail(400, msg);}
别返回 e.getMessage(),那玩意儿是给开发者看的,不是给前端看的。上面的代码取的是校验注解里的 message 属性:@Datapublic class UserDTO {@NotBlank(message = "用户名不能为空")private String username;@Min(value = 1, message = "年龄必须大于0")private Integer age;}
5. 自定义业务异常——层级清晰才能精准兜底

异常体系不分层,到头来只能 catch (Exception e) 一把梭,日志里啥有效信息都没有。public class BusinessException extends RuntimeException {private final int code;public BusinessException(int code, String message) {super(message);this.code = code;}public BusinessException(String message) {this(500, message);}public int getCode() { return code; }}
全局处理器里单独接一下:@ExceptionHandler(BusinessException.class)public Result handleBusinessException(BusinessException e) {log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());return Result.fail(e.getCode(), e.getMessage());}
关键点:这里用 log.warn 不是 log.error。业务异常是预期内的,不应该触发告警。
6. 第三方调用异常——不兜底就等着雪崩

调外部接口超时、熔断、返回异常报文,这些不兜底直接往上抛,调用方收到的就是一堆不可读的异常栈。@ExceptionHandler(HttpClientErrorException.class)public Result handleHttpClientError(HttpClientErrorException e) {log.error("HTTP调用异常: status={}, body={}",e.getStatusCode(), e.getResponseBodyAsString());return Result.fail("服务暂时不可用,请稍后重试");}
如果是 Feign 调用,脱了壳再接一层:@ExceptionHandler(FeignException.class)public Result handleFeignException(FeignException e) {log.error("Feign调用失败: status={}, url={}",e.status(), e.request().url());return Result.fail("远程服务调用异常");}
7. 兜底——接住所有漏网之鱼

上面都接完了,最后还是得有个兜底的:@ExceptionHandler(Exception.class)public Result handleUnknownException(Exception e) {log.error("未捕获异常", e);return Result.fail(500, "系统繁忙,请稍后重试");}
注意顺序:Spring 会找最匹配的 @ExceptionHandler,把 Exception.class 放在最后,具体异常的 handler 放在前面。
8. 日志脱敏——最容易忽略的安全问题

异常堆栈里如果有手机号、身份证、密码,日志打到 ELK 里就是安全事故。加个脱敏工具类:@ExceptionHandler(Exception.class)public Result handleUnknownException(Exception e) {String safeMsg = SensitiveUtil.mask(e.getMessage());log.error("未捕获异常: {}", safeMsg, e);return Result.fail(500, "系统繁忙");}
SensitiveUtil 用正则把手机号中间四位打 *、身份证中间八位打 *,这里不详写,网上轮子很多。
完整类一览
@Slf4j@RestControllerAdvicepublic class GlobalExceptionHandler {// 1. 参数校验@ExceptionHandler(MethodArgumentNotValidException.class)public Result handleValid(MethodArgumentNotValidException e) {String msg = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).collect(Collectors.joining("; "));return Result.fail(400, msg);}// 2. 业务异常@ExceptionHandler(BusinessException.class)public Result handleBusiness(BusinessException e) {log.warn("业务异常: {}", e.getMessage());return Result.fail(e.getCode(), e.getMessage());}// 3. Feign 调用@ExceptionHandler(FeignException.class)public Result handleFeign(FeignException e) {log.error("Feign异常: status={}", e.status());return Result.fail("远程服务调用异常");}// 4. 兜底@ExceptionHandler(Exception.class)public Result handleUnknown(Exception e) {log.error("未捕获异常", e);return Result.fail(500, "系统繁忙,请稍后重试");}}
写在最后

全局异常处理不是什么高大上的东西,但能做到位的项目真不多。总结三个要点:兜底要牢:/error 路径堵死,各种异常类型接
下一页 (1/2)
回帖(0):

全部回帖(0)»
最新回帖
收藏本帖
发新帖