前置关联说明 :在《SpringMVC定制化配置全解》中,我们完成了视图适配、拦截器、跨域、JSON序列化、国际化等请求链路定制,覆盖了请求入参、拦截、响应格式化场景。本文将独立深度拆解SpringMVC 全局统一异常处理,补齐MVC请求闭环最后一环。
一、全局统一异常处理的核心意义
SpringMVC 原生异常机制存在明显的生产缺陷,未做统一处理时,项目会出现接口返回杂乱、代码冗余、安全泄露、运维困难等问题,也是企业开发强制统一规范的核心原因。
原生缺陷汇总:
-
响应格式不统一:业务异常、代码异常、参数异常返回结构完全不同,前端需要多套逻辑适配,维护成本极高。
-
业务代码严重冗余:每个 Controller、Service 都需要手动 try-catch,异常处理逻辑与业务逻辑高度耦合。
-
存在系统安全泄露风险:默认异常会直接返回堆栈信息、类路径、服务器报错详情,暴露项目结构与源码信息。
-
异常无法统一管控:异常散乱抛出,无法实现统一日志打印、统一告警、统一降级提示。
-
用户体验差:原生 500、404、400 错误无友好中文提示,无法适配业务场景。
统一异常处理核心价值 :实现异常解耦、格式统一、安全脱敏、日志可追溯、运维可监控,是前后端分离、微服务、中台项目的基础核心基建。
二、SpringMVC 异常处理完整执行流程
本节细化完整请求异常流转链路,从请求接入到异常捕获、解析、封装、响应返回全流程拆解,是理解底层原理的核心。
2.1 整体执行链路
客户端发起请求 → Tomcat 容器接收请求 → DispatcherServlet 前端控制器分发请求 → 拦截器 preHandle 预处理 → Controller 处理器执行 → Service 业务逻辑执行 → 代码抛出异常 → DispatcherServlet 捕获全局异常 → 委派 HandlerExceptionResolver 解析器链处理 → 匹配自定义全局异常处理器 → 异常信息脱敏、封装标准化结果 → 输出 JSON 响应给前端 → 拦截器 afterCompletion 回调收尾 → 请求结束
2.2 异常匹配核心流程规则
-
异常抛出后,不会直接返回,由 DispatcherServlet 统一拦截,终止异常向外冒泡;
-
遍历 Spring 内置异常解析器链,按照优先级从高到低匹配对应异常处理器;
-
精准异常处理器优先匹配(业务异常 > 参数异常 > 请求异常 > 通用兜底异常);
-
匹配成功后直接执行对应处理逻辑,终止后续解析器遍历;
-
所有异常统一封装为自定义响应体,屏蔽原生异常信息,实现安全脱敏。
三、底层实现原理(Spring 官方规范)
3.1 核心核心组件
SpringMVC 所有异常处理均基于 HandlerExceptionResolver 异常解析器接口实现,框架默认内置三级解析器,优先级由高到低:
-
ExceptionHandlerExceptionResolver(最高优先级) :专门处理
@ExceptionHandler注解方法,是全局异常处理的核心底层支撑。 -
ResponseStatusExceptionResolver :处理标记了
@ResponseStatus注解的自定义状态码异常。 -
DefaultHandlerExceptionResolver(兜底最低优先级):框架默认兜底,处理 404、405、400 等原生 Web 异常。
3.2 全局异常注解原理
@RestControllerAdvice:全局控制器增强注解,SpringMVC 提供的全局切面能力,仅作用于所有 RestController,不依赖 AOP 动态代理,不侵入业务代码。
工作机制:项目启动时自动扫描所有被 @ExceptionHandler 标记的方法,构建异常匹配映射表,请求异常时自动根据异常类型精准匹配对应处理方法。
3.3 优先级终版规则
自定义精准异常处理方法 > 通用兜底异常方法 > Spring 内置默认异常解析器
四、生产级核心实战代码
整套方案包含:统一响应实体、自定义业务异常、全局异常处理器、多类型异常捕获、日志记录、安全脱敏、测试接口,零改造可直接用于生产环境。
4.1 全局统一响应实体
import lombok.Data;
/**
* 全局统一响应实体
* 统一成功/异常返回格式,前后端通用约定
*/
@Data
public class Result<T> {
// 响应状态码:200成功,4xx客户端异常,5xx服务端异常
private Integer code;
// 响应提示信息
private String msg;
// 响应业务数据
private T data;
// 响应时间戳
private long timestamp;
/**
* 成功响应
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
/**
* 失败响应
*/
public static <T> Result<T> error(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
4.2 自定义业务异常
/**
* 自定义业务异常
* 用于区分业务主动抛出异常与系统未知异常
*/
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}
4.3 全局统一异常处理器(核心)
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
/**
* 全局统一异常处理器
* 拦截所有Controller层抛出的异常,统一封装、统一日志、统一返回
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 1. 自定义业务异常(最高优先级,精准匹配业务报错)
*/
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.error("【业务异常】请求地址:{},异常信息:{}", request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 2. JSR303参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
// 拼接所有校验失败信息
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(","));
log.error("【参数校验异常】请求地址:{},异常信息:{}", request.getRequestURI(), errorMsg);
return Result.error(400, "参数校验失败:" + errorMsg);
}
/**
* 3. 请求方式不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<Void> handleMethodException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
log.error("【请求方式异常】请求地址:{},异常信息:{}", request.getRequestURI(), e.getMessage());
return Result.error(405, "当前请求方式不支持");
}
/**
* 4. 全局兜底异常(最低优先级,捕获所有未知异常)
* 生产环境屏蔽堆栈信息,避免安全泄露
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleAllException(Exception e, HttpServletRequest request) {
log.error("【系统未知异常】请求地址:{},异常堆栈:", request.getRequestURI(), e);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
4.4 测试接口代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/exception")
public class ExceptionTestController {
// 测试业务异常
@GetMapping("/business")
public Result<Void> testBusiness() {
throw new BusinessException(4001, "用户权限不足,禁止访问");
}
// 测试系统未知异常
@GetMapping("/system")
public Result<Void> testSystem() {
int num = 1 / 0;
return Result.success(null);
}
}
五、自定义业务异常适配
前文默认使用简易业务异常实现基础报错能力,在复杂生产、微服务、多业务模块场景下,原生简易异常无法区分业务场景、无法精细化匹配处理、错误码不统一 。本节专门扩展企业级自定义异常体系 ,包含:自定义异常层级设计、专属异常匹配流程、枚举统一错误码、精细化异常处理器适配,实现业务异常标准化、可扩展、可精准匹配、可全局管控。
5.1 自定义异常核心设计原理
SpringMVC 全局异常处理器支持自定义异常精准优先级匹配机制 :开发者可基于业务分层自定义异常父类、子类,结合错误码枚举统一规范报错类型,处理器根据异常继承层级、自定义异常类型完成精准匹配,粒度远细于原生 Exception 兜底捕获。
设计核心思想:业务场景分层异常、错误码统一收口、异常精准匹配、差异化响应提示,彻底解决原生异常杂乱、无法细分业务报错的问题。
5.2 自定义异常专属匹配执行流程
该流程区别于默认异常流程,是自定义异常体系的专属流转链路:
-
业务层根据场景主动抛出自定义子类业务异常(权限/参数/账号/业务熔断等);
-
DispatcherServlet 捕获异常,进入异常解析器链;
-
优先匹配
@ExceptionHandler精准自定义异常方法,遵循子类异常优先于父类异常匹配规则; -
匹配成功后读取异常内部自定义错误码、提示信息、业务描述;
-
封装标准化响应体,完成业务提示脱敏、日志分级打印;
-
终止后续异常解析,返回统一 JSON 结果。
5.3 完整核心落地代码
5.3.1 全局统一错误码枚举(统一规范)
/**
* 全局业务错误码枚举
* 统一项目所有业务异常编码,适配前后端、微服务统一解析
*/
public enum BusinessCodeEnum {
// 通用业务异常 4xxx
PARAM_VALID_ERROR(4000, "请求参数校验失败"),
AUTH_ERROR(4001, "用户权限不足,禁止访问"),
REPEAT_SUBMIT(4002, "请勿重复提交请求"),
TOKEN_EXPIRE(4003, "Token已过期,请重新登录"),
// 业务模块异常 5xxx
USER_NOT_EXIST(5001, "用户不存在"),
ORDER_FAIL(5002, "订单创建失败"),
// 系统异常兜底
SYSTEM_ERROR(9999, "系统繁忙,请稍后重试");
private final Integer code;
private final String msg;
BusinessCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
5.3.2 顶层自定义业务父异常
/**
* 全局顶层业务异常父类
* 所有自定义业务异常统一继承此类,方便统一匹配与扩展
*/
public class BaseBusinessException extends RuntimeException {
// 自定义业务错误码
private final Integer code;
// 自定义提示信息
private final String message;
public BaseBusinessException(BusinessCodeEnum codeEnum) {
super(codeEnum.getMsg());
this.code = codeEnum.getCode();
this.message = codeEnum.getMsg();
}
public BaseBusinessException(BusinessCodeEnum codeEnum, String customMsg) {
super(customMsg);
this.code = codeEnum.getCode();
this.message = customMsg;
}
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
5.3.3 细分业务子类异常(按需扩展)
/**
* 权限业务异常
*/
public class AuthBusinessException extends BaseBusinessException {
public AuthBusinessException() {
super(BusinessCodeEnum.AUTH_ERROR);
}
public AuthBusinessException(String customMsg) {
super(BusinessCodeEnum.AUTH_ERROR, customMsg);
}
}
/**
* 参数校验业务异常
*/
public class ParamBusinessException extends BaseBusinessException {
public ParamBusinessException() {
super(BusinessCodeEnum.PARAM_VALID_ERROR);
}
public ParamBusinessException(String customMsg) {
super(BusinessCodeEnum.PARAM_VALID_ERROR, customMsg);
}
}
5.3.4 适配自定义异常的增强全局异常处理器
基于原有处理器改造,新增分层自定义异常精准匹配,优先级:子类细分异常 > 顶层父异常 > 参数异常 > 系统异常
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// ====================== 自定义分层业务异常(最高优先级)======================
/**
* 权限异常精准匹配
*/
@ExceptionHandler(AuthBusinessException.class)
public Result<Void> handleAuthException(AuthBusinessException e, HttpServletRequest request) {
log.warn("【权限异常】路径:{},信息:{}", request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 参数业务异常精准匹配
*/
@ExceptionHandler(ParamBusinessException.class)
public Result<Void> handleParamBusinessException(ParamBusinessException e, HttpServletRequest request) {
log.warn("【业务参数异常】路径:{},信息:{}", request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 通用顶层业务异常兜底
*/
@ExceptionHandler(BaseBusinessException.class)
public Result<Void> handleBaseBusinessException(BaseBusinessException e, HttpServletRequest request) {
log.warn("【通用业务异常】路径:{},信息:{}", request.getRequestURI(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
// ====================== 原生框架异常 ======================
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(","));
log.error("【参数校验异常】路径:{},信息:{}", request.getRequestURI(), errorMsg);
return Result.error(400, "参数校验失败:" + errorMsg);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Result<Void> handleMethodException(HttpRequestMethodNotSupportedException e, HttpServletRequest request) {
log.warn("【请求方式异常】路径:{}", request.getRequestURI());
return Result.error(405, "当前请求方式不支持");
}
// ====================== 系统全局兜底 ======================
@ExceptionHandler(Exception.class)
public Result<Void> handleAllException(Exception e, HttpServletRequest request) {
log.error("【系统未知异常】路径:{},堆栈:", request.getRequestURI(), e);
return Result.error(9999, "系统繁忙,请稍后重试");
}
}
5.3.5 业务使用 & 测试接口代码
@RestController
@RequestMapping("/api/exception")
public class ExceptionTestController {
// 测试细分权限异常
@GetMapping("/auth")
public Result<Void> testAuthError() {
throw new AuthBusinessException("当前账号无操作权限,请联系管理员");
}
// 测试细分参数异常
@GetMapping("/param")
public Result<Void> testParamError() {
throw new ParamBusinessException("手机号格式非法,请检查");
}
}
5.4 自定义异常适配适用场景
-
中大型企业项目:需要统一错误码规范、分层管理业务报错,方便前后端联调、问题定位;
-
微服务分布式项目:多服务统一异常枚举,避免各服务错误码混乱、响应格式不一致;
-
对外开放平台:标准化错误码、细分异常类型,方便第三方对接、异常归类统计;
-
需要精细化告警运维场景:不同业务异常分类打印日志、可对接告警平台实现差异化告警。
5.5 自定义异常适配优缺点分析
5.5.1 优点
-
异常粒度精细化:区分权限、参数、订单、用户等不同业务异常,精准匹配、差异化处理;
-
错误码全局统一:通过枚举收口所有业务报错,杜绝随机编码、提示混乱问题;
-
可扩展性极强:新增业务场景仅需新增子类异常、补充枚举,无需修改核心处理器;
-
运维友好:分层日志打印,业务异常warn级别、系统异常error级别,日志干净易排查;
-
适配微服务:统一异常体系可无缝适配Feign远程调用、网关统一解析。
5.5.2 缺点
-
初期开发成本略高:需要维护异常枚举、父类、子类体系,小型简单项目略显冗余;
-
规范要求严格:业务代码必须统一抛出自定义异常,若混用原生异常会破坏统一体系;
-
子类过多管理成本上升:超大项目需按模块分包管理异常类,否则类文件泛滥。
5.6 自定义异常匹配优先级终版总结
细分子类自定义异常 > 顶层通用业务异常 > 框架原生异常 > 系统全局兜底异常
六、ErrorController 定制化兜底改造(解决404/405原生页面兜底漏洞)
前文所有 @RestControllerAdvice 全局异常处理器仅能拦截 Controller 执行过程中抛出的异常 ,无法拦截 SpringMVC 原生 WEB 基础异常:404路径不存在、405请求方式不支持、400参数解析失败等。这类请求不会进入 Controller 方法,导致全局异常处理器失效、返回原生白色错误页面/原生JSON,造成前后端格式不统一、生产报错错乱问题。
SpringBoot 提供 ErrorController 全局兜底错误控制器,是 Web 层级最后一道异常兜底屏障,本节完成企业级定制化改造,实现「RestControllerAdvice业务异常拦截 + ErrorController 原生WEB异常兜底」双闭环全覆盖。
6.1 原生默认机制缺陷(必须改造的原因)
-
拦截范围断层:404、405、400、500容器级异常不进入Controller,全局异常注解处理器无法捕获;
-
返回格式不统一:浏览器访问返回默认WhiteLabel白页,接口访问返回原生错误JSON,与项目统一Result格式冲突;
-
存在安全隐患:默认错误响应会携带服务器路径、请求参数、堆栈概要,泄露项目信息;
-
无法统一运维:原生异常无统一日志、无统一错误码、无法对接告警平台。
6.2 ErrorController 底层实现原理
根据 SpringBoot 官方机制:所有未被 Controller 捕获、未被异常解析器处理的 WEB 错误,最终都会转发到 /error 请求路径,由默认 BasicErrorController 统一处理。
开发者可通过自定义实现 ErrorController 接口 + 覆盖 /error 端点,替换系统默认兜底控制器,实现全网所有请求异常统一格式化返回。
双兜底最终优先级:自定义ControllerAdvice精准异常拦截 > 自定义ErrorController全局WEB兜底 > Spring默认错误处理器
6.3 定制化改造完整执行流程
-
客户端发起非法请求(不存在路径/错误请求方式);
-
DispatcherServlet 分发请求,匹配不到有效 Handler 或请求预处理失败;
-
不会进入自定义全局异常处理器(RestControllerAdvice失效);
-
Spring 自动转发至内部
/error端点; -
自定义 ErrorController 接管错误请求,获取错误状态码、请求信息;
-
统一封装为项目 Result 标准响应、分级打印日志、脱敏原生错误信息;
-
返回标准JSON,完成全网异常统一兜底。
6.4 ErrorController 定制化核心代码
6.4.1 自定义全局兜底错误控制器
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* 全局兜底错误控制器(定制化改造)
* 专门兜底 404/405/400 等所有无法进入Controller的WEB异常
* 补足 @RestControllerAdvice 拦截盲区,实现全网异常统一格式
*/
@Slf4j
@RestController
public class GlobalErrorController implements ErrorController {
private static final String ERROR_PATH = "/error";
/**
* 接管所有WEB底层错误
*/
@RequestMapping(ERROR_PATH)
public Result<Void> handleError(HttpServletRequest request) {
// 获取原生HTTP状态码
Integer status = (Integer) request.getAttribute("javax.servlet.error.status_code");
// 根据状态码自定义标准化提示
if (HttpStatus.NOT_FOUND.value() == status) {
log.warn("【WEB兜底异常】404路径不存在,请求地址:{}", request.getRequestURI());
return Result.error(404, "请求资源不存在");
}
if (HttpStatus.METHOD_NOT_ALLOWED.value() == status) {
log.warn("【WEB兜底异常】405请求方式不支持,请求地址:{}", request.getRequestURI());
return Result.error(405, "当前请求方式不被支持");
}
if (HttpStatus.BAD_REQUEST.value() == status) {
log.warn("【WEB兜底异常】400参数错误,请求地址:{}", request.getRequestURI());
return Result.error(400, "请求参数非法");
}
// 全局未知WEB异常兜底
log.error("【WEB系统异常】服务器未知错误,状态码:{},请求地址:{}", status, request.getRequestURI());
return Result.error(500, "服务器繁忙,请稍后重试");
}
/**
* 重写错误路径,覆盖默认控制器
*/
@Override
public String getErrorPath() {
return ERROR_PATH;
}
}
6.5 双兜底联动完整架构说明
改造完成后,项目形成双层异常防护闭环,彻底消除异常拦截盲区:
-
第一层(业务层拦截):@RestControllerAdvice拦截:进入Controller后的业务异常、参数校验异常、代码运行异常;
-
特点:精准匹配、分级处理、业务精细化提示。
第二层(容器层兜底):自定义ErrorController拦截:404、405、400、请求预处理失败、无匹配处理器等所有WEB底层错误;
特点:拦截所有盲区请求,全网统一响应格式。
6.6 ErrorController 改造适用场景
-
前后端分离项目必备:杜绝前端收到白页、原生错误JSON,全网接口格式统一;
-
对外开放API平台:非法路径、恶意爬虫扫描统一友好返回,保护服务信息;
-
微服务网关下游服务:避免无效请求报错泛滥,统一日志与错误码;
-
生产严格规范项目:消除所有异常拦截盲区,满足运维监控、日志统计规范。
6.7 定制化改造优缺点分析
6.7.1 优点
-
彻底消除拦截盲区:补足全局异常处理器短板,实现100%请求异常全覆盖;
-
全网响应格式统一:无论是业务报错还是容器级报错,结构完全一致;
-
安全性提升:屏蔽原生错误堆栈、服务器信息,脱敏对外错误提示;
-
运维统一可控:所有异常统一日志、统一错误码,可正常接入告警与链路追踪。
6.7.2 缺点与避坑点
-
需要手动维护状态码:新增特殊HTTP状态码需要手动补充判断逻辑;
-
避免重复拦截:正常Controller异常已被Advice拦截,不会进入ErrorController,无需担心重复处理;
-
禁止自定义跳转页面:前后端分离项目必须返回JSON,不能返回视图,否则适配前端失败。
6.8 全网最终异常优先级(完整版)
细分自定义业务异常 > 通用业务异常 > 框架参数异常 > 系统兜底异常 > 自定义ErrorController容器级兜底 > Spring原生默认错误机制