接口写多了以后,最怕的不是异常本身,而是异常的形状不一致:有的返回 Tomcat 默认错误页,有的把完整堆栈给前端,有的只在日志里留一句模糊提示。这个项目把
WebMVC 服务的异常处理收敛到 service-common 的 GlobalExceptionHandler,业务模块引入公共包后自动生效。
一、处理边界
处理器使用 @RestControllerAdvice,优先级可以理解为:
BusinessException
参数校验异常
请求格式 / 路径 / 方法异常
未预期异常兜底
这个顺序很重要。业务异常最了解自己,应该原样透传 code 和 message;框架异常则应该翻译成统一的 ApiResponse,避免前端理解
Spring 内部异常。
二、把校验错误拼成可读信息
@RequestBody 校验失败、表单绑定失败、@RequestParam 约束失败,最终都被转换成类似下面的提示:
username: 用户名不能为空; age: 年龄必须在 1 到 120 之间
核心做法是从 FieldError 或 ConstraintViolation 中取出字段和默认消息,再用分号拼接。这样前端可以直接展示,不需要解析异常类名。
三、框架异常的分类
常见异常都有明确归属:
HttpMessageNotReadableException:请求体 JSON 不合法。MissingServletRequestParameterException:缺少必填参数。NoResourceFoundException:路径不存在。HttpRequestMethodNotSupportedException:请求方法不匹配。
其中 favicon.ico 的 404 会被静默处理。浏览器会自动请求它,如果每次都打 warn,日志很快就会被无价值信息填满。
四、兜底异常不要泄露堆栈
未预期异常统一记录完整堆栈,但响应里只返回稳定结构和提示:
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("未预期异常", e);
String message = e.getMessage() != null
? e.getMessage()
: ApiResponseEnum.INTERNAL_ERROR.message();
return ApiResponse.failure(ApiResponseEnum.INTERNAL_ERROR.code(), message);
}
完整堆栈给开发者,稳定 JSON 给调用方,这是全局异常处理的基本分工。
五、经验总结
全局异常处理适合放在公共模块,但不代表业务代码可以到处 try-catch。业务失败优先抛 BusinessException
;只有需要转换错误、补充上下文或清理资源时,才在局部捕获。