Spring 异常处理终极版(2025 最新生产级写法)
99% 的项目都只用这一套就完全够了,统一、优雅、可维护、可监控,直接复制到项目里开箱即用!
一、2025 年真实项目推荐层级(从外到内)
| 层级 | 处理方式 | 作用范围 | 推荐指数 |
|---|---|---|---|
| 1 | Filter(最外层) | 所有请求(包括静态资源) | 1 star |
| 2 | Spring Security ExceptionTranslationFilter | 认证/授权异常 | 5 stars |
| 3 | @ControllerAdvice + @ExceptionHandler | Spring MVC 所有 Controller | 5 stars |
| 4 | HandlerExceptionResolver(底层) | 兜底 | 内部使用 |
结论:99.9% 的项目只需要 @ControllerAdvice 就完美解决所有异常处理!
二、生产级统一异常处理完整代码(直接复制)
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高(可选)
@Slf4j
public class GlobalExceptionHandler {
/** 1. 校验异常(@Valid、@Validated) */
@ExceptionHandler(BindException.class)
public R<String> handleBindException(BindException e) {
String msg = e.getAllErrors().get(0).getDefaultMessage();
log.warn("参数校验失败: {}", msg);
return R.error(400, "参数错误:" + msg);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<String> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
log.warn("JSON 参数校验失败: {}", msg);
return R.error(400, msg);
}
/** 2. 自定义业务异常(最常用!) */
@ExceptionHandler(BusinessException.class)
public R<String> handleBusiness(BusinessException e) {
log.warn("业务异常: code={}, msg={}", e.getCode(), e.getMessage());
return R.error(e.getCode(), e.getMessage());
}
/** 3. 权限相关 */
@ExceptionHandler(AccessDeniedException.class)
public R<String> handleAccessDenied(AccessDeniedException e) {
return R.error(403, "无权限访问");
}
/** 4. 404 - 资源不存在 */
@ExceptionHandler(NoHandlerFoundException.class)
public R<String> handleNotFound(NoHandlerFoundException e) {
return R.error(404, "接口不存在:" + e.getRequestURL());
}
/** 5. 文件上传大小超限 */
@ExceptionHandler(MaxUploadSizeExceededException.class)
public R<String> handleMaxUploadSize(MaxUploadSizeExceededException e) {
return R.error(413, "文件太大,最大支持 100MB");
}
/** 6. 通用运行时异常(兜底) */
@ExceptionHandler(Exception.class)
public R<String> handleException(Exception e) {
log.error("未捕获异常", e);
String traceId = IdUtil.fastSimpleUUID(); // hutool
return R.error(500, "服务器异常,请稍后重试(traceId: " + traceId + ")");
}
}
三、自定义业务异常体系(强烈推荐!)
// 基础业务异常
@Getter
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ResultCode resultCode) {
super(resultCode.getMsg());
this.code = resultCode.getCode();
}
}
// 常用枚举码(放在 constants 包)
@Getter
public enum ResultCode {
SUCCESS(200, "操作成功"),
FAIL(500, "操作失败"),
PARAM_ERROR(1001, "参数错误"),
USER_NOT_FOUND(1002, "用户不存在"),
LOGIN_EXPIRED(1003, "登录已过期"),
STOCK_NOT_ENOUGH(1004, "库存不足"),
ORDER_NOT_FOUND(1005, "订单不存在");
private final int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
使用方式(Controller/Service 里直接扔):
if (user == null) {
throw new BusinessException(ResultCode.USER_NOT_FOUND);
}
if (stock < num) {
throw new BusinessException(ResultCode.STOCK_NOT_ENOUGH);
}
四、统一响应体 R(所有接口都返回这个)
@Getter
@Setter
@NoArgsConstructor
public class R<T> implements Serializable {
private int code = 200;
private String msg = "success";
private T data;
public static <T> R<T> ok() {
return new R<>();
}
public static <T> R<T> ok(T data) {
R<T> r = new R<>();
r.setData(data);
return r;
}
public static <T> R<T> error(int code, String msg) {
R<T> r = new R<>();
r.setCode(code);
r.setMsg(msg);
return r;
}
public static <T> R<T> error(ResultCode resultCode) {
return error(resultCode.getCode(), resultCode.getMsg());
}
}
五、Spring Boot 3 额外配置(让 404 也能被 @ControllerAdvice 捕获)
# application.yml
spring:
mvc:
throw-exception-if-no-handler-found: true # 关键!默认 false
web:
resources:
add-mappings: false # 防止静态资源 404 被吞掉(可选)
六、2025 年最优雅写法(支持返回指定状态码)
@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(HttpStatus.CONFLICT) // 返回 409
public R<String> handleDuplicateKey(DuplicateKeyException e) {
return R.error(409, "数据已存在");
}
或者使用 ResponseEntity:
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<R<String>> handleAccessDenied() {
return ResponseEntity.status(403).body(R.error(403, "无权限"));
}
七、真实项目最终效果(前后端都爽)
// 正常返回
{
"code": 200,
"msg": "success",
"data": {...}
}
// 业务异常
{
"code": 1002,
"msg": "用户不存在",
"data": null
}
// 参数错误
{
"code": 400,
"msg": "参数错误:用户名不能为空",
"data": null
}
// 服务器异常
{
"code": 500,
"msg": "服务器异常,请稍后重试(traceId: a1b2c3d4e5)",
"data": null
}
八、避坑指南
| 坑点 | 解决方案 |
|---|---|
| @Valid 校验异常没生效 | 方法参数要加 @Valid + BindingResult,或直接用 @ControllerAdvice 捕获 MethodArgumentNotValidException |
| 404 接口没被捕获 | 配置 throw-exception-if-no-handler-found: true |
| 想返回自定义状态码 | 加 @ResponseStatus 或返回 ResponseEntity |
| 多个 @ControllerAdvice 冲突 | 加 @Order(Ordered.HIGHEST_PRECEDENCE) 控制顺序 |
| 想区分开发/生产环境返回堆栈 | 加 profile 判断 |
现在你已经拥有了企业级最优雅、最完整的异常处理体系!
下一步你想看哪个实战?
- 结合 Spring Validation 做分组校验 + 自定义校验器
- 集成 Sentry/ELK 异常监控 + 钉钉报警
- 结合 Spring Security 的权限异常处理
- 401/403 统一处理
- 异步请求(@Async)异常如何捕获
直接告诉我,我继续给你写 10 万字级干货!