全局异常处理与参数校验概述接口报错的时候如果直接让异常往外抛前端拿到的是一坨 Tomcat 默认的错误页面 HTML或者一串乱七八糟的堆栈信息。前端没法判断这是什么错、该提示用户什么。全局异常处理就是兜底不管哪里出了什么错最后返回给前端的始终是统一的 JSON 结构——有错误码、有错误信息前端可以稳稳地拿这个做处理。参数校验也一起搞了Controller 里别写一堆if (xxx null)靠注解声明规则校验不通过直接抛异常由全局处理器统一拦下来返回。引入依赖ControllerAdvice、ExceptionHandler这些来自spring-boot-starter-web如果你已经用了 Web 模块就不用额外加了。但是参数校验需要单独引入dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-validation/artifactId/dependencySpring Boot 2.x 时代validation内嵌在spring-boot-starter-web里3.x 拆出来了。不加这个依赖的话NotBlank、Valid不会报编译错但运行时校验不生效——加了注解等于白加所有参数直接放行。第一步搭异常体系common我们需要两个基础的东西异常接口 异常类。放在common模块全项目都能用。BaseExceptionInterfacepublicinterfaceBaseExceptionInterface{StringgetErrorCode();StringgetErrorMessage();}就是一个接口规定异常必须包含错误码和错误信息。后面我们的枚举实现它BizException也依赖它。BizExceptionBizException代表业务异常每个模块都可能出现业务异常所以定义在common模块里面。GetterSetterpublicclassBizExceptionextendsRuntimeException{privateStringerrorCode;privateStringerrorMessage;publicBizException(BaseExceptionInterfacebaseExceptionInterface){this.errorCodebaseExceptionInterface.getErrorCode();this.errorMessagebaseExceptionInterface.getErrorMessage();}}继承RuntimeException这样在业务代码里抛的时候不用在方法签名上throwsSpring 也默认对运行时异常做事务回滚。构造参数只接收BaseExceptionInterface不让你随便new BizException(随便写的错)——所有异常必须预先在枚举里定义好方便统一管理。具体业务模块定义我这个模块会有什么异常ResponseCodeEnumGetterAllArgsConstructorpublicenumResponseCodeEnumimplementsBaseExceptionInterface{// 通用异常SYSTEM_ERROR(AUTH-10000,出错啦后台小哥正在努力修复中...),PARAM_NOT_VALID(AUTH-10001,参数错误),// 业务异常往后加;privatefinalStringerrorCode;privatefinalStringerrorMessage;}这里其实很巧妙这里是implements BaseExceptionInterface但是这个接口定义的是getErrorCode和getErrorMessage两个方法我这里定义了两个变量加上Getter刚好重写了接口的两个方法。要点说明实现BaseExceptionInterface枚举实例可以直接丢进BizException的构造参数错误码前缀AUTH代表这个模块和其他模块的错误码区分开。比如订单模块用ORDER-20000后面加什么登录失败、用户不存在、权限不足……随着业务迭代直接在枚举里加就行抛一个业务异常就一句话thrownewBizException(ResponseCodeEnum.SYSTEM_ERROR);三者关系BizException的构造参数是BaseExceptionInterface接口不是ResponseCodeEnum具体枚举。所以BizException 构造参数 ↓ 依赖 BaseExceptionInterface接口规定必须有 errorCode errorMessage ↑ 实现 ResponseCodeEnum枚举集中管理所有错误码接口起约定作用不管哪个模块、哪个枚举只要你实现了这两个 getter就能丢进BizException枚举起集中管理作用所有错误码一处定义不散落BizException只认接口不认枚举auth 模块的AuthResponseCodeEnum、order 模块的OrderResponseCodeEnum只要实现了接口都能用如果直接用new BizException(String, String)错误码会散落在各个角落改一个提示语得全项目搜想看看系统里有哪些错误码也无从下手。第二步全局异常处理器ControllerAdviceSlf4jpublicclassGlobalExceptionHandler{/** 业务异常 */ExceptionHandler({BizException.class})ResponseBodypublicResponseObjecthandleBizException(HttpServletRequestrequest,BizExceptione){log.warn({} request fail, errorCode: {}, errorMessage: {},request.getRequestURI(),e.getErrorCode(),e.getErrorMessage());returnResponse.fail(e);}/** 参数校验失败 */ExceptionHandler({MethodArgumentNotValidException.class})ResponseBodypublicResponseObjecthandleMethodArgumentNotValidException(HttpServletRequestrequest,MethodArgumentNotValidExceptione){StringerrorCodeResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();BindingResultbindingResulte.getBindingResult();StringBuildersbnewStringBuilder();Optional.ofNullable(bindingResult.getFieldErrors()).ifPresent(errors-{errors.forEach(error-sb.append(error.getField()).append( ).append(error.getDefaultMessage()).append(, 当前值: ).append(error.getRejectedValue()).append(; ));});log.warn({} request error, errorCode: {}, errorMessage: {},request.getRequestURI(),errorCode,sb.toString());returnResponse.fail(errorCode,sb.toString());}/** 其他未捕获的异常——兜底 */ExceptionHandler({Exception.class})ResponseBodypublicResponseObjecthandleOtherException(HttpServletRequestrequest,Exceptione){log.error({} request error,request.getRequestURI(),e);returnResponse.fail(ResponseCodeEnum.SYSTEM_ERROR);}}三个关键注解注解干什么的ControllerAdvice全局增强所有 Controller 的异常都会被这里的ExceptionHandler拦截ExceptionHandler声明这个方法专门处理哪种类型的异常ResponseBody返回的内容直接序列化为 JSON不要走视图模板三层兜底结构Exception其他所有异常 ← 第三层保底全拦住了 └─ MethodArgumentNotValidException ← 第二层参数校验 └─ BizException ← 第一层业务异常Spring 匹配规则是找最精确的。如果抛的是BizException只走第一个 handler抛的是MethodArgumentNotValidException走第二个其他任何没被前两个匹配到的异常都掉进第三个兜底。三层覆盖不会有漏网之鱼。注意BizException用log.warn因为它是业务可预期的错不需要大惊小怪。最后那个Exception用log.error并打印了堆栈因为这是真正该修 bug 的异常。第三步参数校验上面那个MethodArgumentNotValidException是什么时候抛的当加了Valid的入参校验不通过时。在实体类属性上加校验注解DatapublicclassUser{NotBlank(message昵称不能为空)privateStringnickName;privateLocalDateTimecreateTime;}在 Controller 方法参数里加 ValidPostMapping(/test2)ApiOperationLog(description测试接口2)publicResponseUsertest2(ValidRequestBodyUseruser){returnResponse.success(user);}注意Valid是加在方法参数上的不是加在类上。二者缺一不可类上没有NotBlank→ 不会校验什么都过得去方法参数没有Valid→ 不会触发校验注解成了摆设校验错误信息是怎么拼出来的GlobalExceptionHandler里的handleMethodArgumentNotValidException遍历了bindingResult.getFieldErrors()把每条错误的字段名、校验描述、实际收到的值拼接在一起sb: nickName 昵称不能为空, 当前值: ; 这样前端/开发人员一看就知道是哪个字段、为什么不通过、实际传了什么。常用校验注解注解校验规则NotBlank不能为null且不能为空字符串不行 也不行NotNull不能为null但可以NotEmpty不能为null且不能为空集合/空字符串Email必须是邮箱格式Size(min, max)字符串/集合长度范围Min/Max数字最小值/最大值Pattern(regexp)自定义正则校验选哪个要看业务场景比如昵称不允许空字符串用NotBlank而不是NotNull因为NotNull允许通过。完整流程串联以一个参数校验失败的请求为例整个链路前端 POST/test2body 是{ nickName: }Spring 序列化 JSON →User对象Valid触发校验 →NotBlank校验失败Spring 抛出MethodArgumentNotValidExceptionControllerAdvice的handleMethodArgumentNotValidException拦住遍历错误字段拼出nickName 昵称不能为空, 当前值: ; 返回Response.fail(AUTH-10001, ...)→ JSON前端从头到尾拿到的都是统一结构一个if (response.success)就能判断是否正常。如何使用业务异常你想手动中断一个请求、返回错误信息给前端// 不满足条件直接抛if(usernull){thrownewBizException(ResponseCodeEnum.SYSTEM_ERROR);}在ResponseCodeEnum里加新的异常码// 通用异常SYSTEM_ERROR(AUTH-10000,出错啦后台小哥正在努力修复中...),PARAM_NOT_VALID(AUTH-10001,参数错误),// 业务异常LOGIN_FAILED(AUTH-20001,用户名或密码错误),USER_NOT_FOUND(AUTH-20002,用户不存在),UNAUTHORIZED(AUTH-20003,没有访问权限),;然后thrownewBizException(ResponseCodeEnum.LOGIN_FAILED);