你的 @RestControllerAdvice 真的兜住了所有异常吗?线上告警满天飞,日志里全是堆栈,接口返回时而 JSON 时而 HTML——这些问题,说到底都是异常处理没做对。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.fail("系统繁忙");
}
}
这代码初看没毛病,上线就跑偏——JSON 解析异常根本进不来。
Spring 在处理请求体反序列化时,
HttpMessageNotReadableException 抛出时机早于 Controller 方法调用,默认的 /error 路径会给你返回一个 HTML 页面。前端直接裂开。
Spring Boot 的 BasicErrorController 是万恶之源。你定义了 @RestControllerAdvice,但某些异常绕过它走了 /error,返回的就是 HTML。
@RestController
public 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 要放在能被扫描到的包里,不然还是不生效。
Map
@Data
@NoArgsConstructor
@AllArgsConstructor
public 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。
这个坑踩过的人最多:参数上加了 @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 属性:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Min(value = 1, message = "年龄必须大于0")
private Integer age;
}
异常体系不分层,到头来只能 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。业务异常是预期内的,不应该触发告警。
调外部接口超时、熔断、返回异常报文,这些不兜底直接往上抛,调用方收到的就是一堆不可读的异常栈。
@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("远程服务调用异常");
}
上面都接完了,最后还是得有个兜底的:
@ExceptionHandler(Exception.class)
public Result<?> handleUnknownException(Exception e) {
log.error("未捕获异常", e);
return Result.fail(500, "系统繁忙,请稍后重试");
}
注意顺序:Spring 会找最匹配的 @ExceptionHandler,把 Exception.class 放在最后,具体异常的 handler 放在前面。
异常堆栈里如果有手机号、身份证、密码,日志打到 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
@RestControllerAdvice
public 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, "系统繁忙,请稍后重试");
}
}
全局异常处理不是什么高大上的东西,但能做到位的项目真不多。总结三个要点:
照这个模板改完,Ctrl+S 一把,至少能少接一半的线上告警电话。
下一篇聊聊 @Transactional 的 5 个致命坑,关注不走丢。
更新时间:2026-06-01
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302035593号