校验异常处理:c.w.exception.GlobalExceptionHandler : 异常错误: generateWorkflow.createDTOs[0].payMode: 支出方式
前端





后端代码:使用@Validated
package com.weiyu.controller;
import com.weiyu.model.CapitalAllocateCreateDTO;
import com.weiyu.model.CapitalAllocateDetailVO;
import com.weiyu.model.Result;
import com.weiyu.service.CapitalAllocateService;
import com.weiyu.util.SecurityUtils;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 资金分配控制器
*/
@RestController
@RequestMapping("/capital/allocate")
@RequiredArgsConstructor
@Slf4j
@Validated
public class CapitalAllocateController {
private final CapitalAllocateService allocateService;
/**
* 生成资金分派工作流程
*
* @param capitalId 资金信息id
* @param createDTOs 资金分配明细列表
* @return {@link Result}<{@link Void}>
*/
@PostMapping("/generate-workflow/{capitalId}")
public Result<Void> generateWorkflow(
@PathVariable @Min(value = 1, message = "资金信息id不能小于1") Integer capitalId,
@RequestBody @Valid List<CapitalAllocateCreateDTO> createDTOs
) {
String endpoint = "/capital/allocate/generate-workflow/";
String method = "generateWorkflow";
log.info("【资金分配】生成资金分配工作流程,{}{},{},createDTOs = {}",
endpoint, capitalId, method, SecurityUtils.safeForLog(createDTOs));
try {
allocateService.generateWorkflow(capitalId, createDTOs);
return Result.success();
} catch (Exception e) {
log.error("【资金分配】生成资金分配工作流程失败,{},{}", endpoint, method, e);
return Result.error("生成资金分配工作流程失败");
}
}
}
后端异常堆栈信息:
c.w.exception.GlobalExceptionHandler : 异常错误: generateWorkflow.createDTOs[0].payMode: 支出方式不能为空, generateWorkflow.createDTOs[1].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].budget: 预算情况不能为空, generateWorkflow.createDTOs[0].payType: 支出分类不能为空, generateWorkflow.createDTOs[0].budget: 预算情况不能为空, generateWorkflow.createDTOs[1].payMode: 支出方式不能为空, generateWorkflow.createDTOs[0].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].payType: 支出分类不能为空
2026-02-12T12:29:21.858+08:00 ERROR 6192 --- [nio-8080-exec-2] c.w.exception.GlobalExceptionHandler : 异常错误: generateWorkflow.createDTOs[0].payMode: 支出方式不能为空, generateWorkflow.createDTOs[1].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].budget: 预算情况不能为空, generateWorkflow.createDTOs[0].payType: 支出分类不能为空, generateWorkflow.createDTOs[0].budget: 预算情况不能为空, generateWorkflow.createDTOs[1].payMode: 支出方式不能为空, generateWorkflow.createDTOs[0].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].payType: 支出分类不能为空
jakarta.validation.ConstraintViolationException: generateWorkflow.createDTOs[0].payMode: 支出方式不能为空, generateWorkflow.createDTOs[1].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].budget: 预算情况不能为空, generateWorkflow.createDTOs[0].payType: 支出分类不能为空, generateWorkflow.createDTOs[0].budget: 预算情况不能为空, generateWorkflow.createDTOs[1].payMode: 支出方式不能为空, generateWorkflow.createDTOs[0].deptId: 指标使用部门不能为空, generateWorkflow.createDTOs[1].payType: 支出分类不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:170) ~[spring-context-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:58) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) ~[spring-aop-6.1.15.jar:6.1.15]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720) ~[spring-aop-6.1.15.jar:6.1.15]
at com.weiyu.controller.CapitalAllocateController$$SpringCGLIB$$0.generateWorkflow(<generated>) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.15.jar:6.1.15]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[spring-webmvc-6.1.15.jar:6.1.15]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[tomcat-embed-core-10.1.33.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.15.jar:6.1.15]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.33.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.15.jar:6.1.15]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.15.jar:6.1.15]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.33.jar:10.1.33]
at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
这是一个 ConstraintViolationException(约束违例异常),属于 Jakarta Bean Validation 规范。它在您的场景中由 Controller 方法参数验证失败 触发,并且不是通过 @Valid + @RequestBody 触发的,而是通过 @Validated 注解在 Controller 类上 激活的 Spring AOP 方法级验证。
一、异常定位与原因
1. 异常栈关键信息
-
异常类型:
jakarta.validation.ConstraintViolationException -
抛出位置:
MethodValidationInterceptor.invoke(Spring AOP 拦截器) -
错误消息:
generateWorkflow.createDTOs[0].deptId: 指标使用部门不能为空等 -
目标方法:
CapitalAllocateController.generateWorkflow(被 CGLIB 代理)
2. 为什么会抛出此异常?
您的 Controller 类上标注了 @Validated,类似:
java
@RestController
@Validated // ← 关键!激活 AOP 方法级验证
public class CapitalAllocateController {
@PostMapping("/generateWorkflow")
public Result generateWorkflow(@RequestBody GenerateWorkflowDTO generateWorkflow) {
// ...
}
}
此时 Spring 会为目标类创建 AOP 代理,在方法执行前校验所有方法参数上直接标注的约束注解(包括 @NotNull、@NotBlank 等),以及参数对象内部被 @Valid 标记的嵌套属性。由于 GenerateWorkflowDTO 中包含 List<CreateDTO> createDTOs,且 CreateDTO 的字段上有 @NotNull 等约束,验证失败时便会抛出 ConstraintViolationException。
二、与前一个异常(HandlerMethodValidationException)的区别
| 维度 | 本次异常 | 上次异常 |
|---|---|---|
| 异常类型 | ConstraintViolationException |
HandlerMethodValidationException |
| 触发方式 | @Validated 类注解 + AOP 拦截 |
Spring 6.1+ 内置 HandlerMethod 参数校验(自动) |
| 校验对象 | 方法参数(复杂 JavaBean + 嵌套) | 方法参数(简单类型,如 @RequestParam String name) |
| 异常来源 | spring-context 中的验证拦截器 |
spring-web 中的 HandlerMethodValidator |
| 是否需要注解 | 必须在类上标注 @Validated |
无需任何注解,Spring 6.1 默认启用 |
| 嵌套验证支持 | 需在字段上加 @Valid,自动递归 |
不适用(针对简单类型) |
| 全局捕获建议 | @ExceptionHandler(ConstraintViolationException.class) |
@ExceptionHandler(HandlerMethodValidationException.class) |
简单概括:
-
上次报错是因为方法参数直接写了
@NotBlank String name,Spring 6.1 自动校验,抛HandlerMethodValidationException。 -
本次报错是因为Controller 类上有
@Validated,Spring AOP 介入,验证@RequestBody对象的嵌套属性,抛ConstraintViolationException。
三、如何处理这个异常?
您的全局异常处理器目前可能没有捕获 ConstraintViolationException,导致它被默认日志记录并返回 500(或 400 但无友好提示)。需要显式添加处理方法:
java
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 捕获 @Validated + Controller 类上的方法参数校验异常
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleConstraintViolationException(
ConstraintViolationException ex) {
List<String> errors = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", 400);
body.put("errors", errors);
return ResponseEntity.badRequest().body(body);
}
}
四、其他需要注意的问题
1. 嵌套集合的验证路径
您的错误信息显示 generateWorkflow.createDTOs[0].deptId,说明:
-
GenerateWorkflowDTO中有List<CreateDTO> createDTOs。 -
CreateDTO的字段上标注了@NotNull(message = "指标使用部门不能为空")等。 -
且
GenerateWorkflowDTO的createDTOs字段上必须标注@Valid,否则不会触发嵌套验证:java
public class GenerateWorkflowDTO { @Valid // 必须! private List<CreateDTO> createDTOs; }
2. 两种验证方式共存时的异常区分
如果您同时使用了:
-
Spring 6.1 默认的参数验证(简单类型)
-
类级
@Validated的方法参数验证
那么全局异常处理器需要同时捕获 HandlerMethodValidationException 和 ConstraintViolationException,并分别处理。
3. 是否必须保留类级 @Validated?
不一定。您也可以选择:
-
移除
@Validated,改用@Valid配合@RequestBody,这样验证失败会抛出MethodArgumentNotValidException,由 Spring MVC 默认处理(通常已经配置过)。 -
保留
@Validated但统一异常处理,如上所示。
4. 建议的异常处理方案(Spring Boot 3.2+)
java
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. 处理 @RequestBody + @Valid
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) { ... }
// 2. 处理 @ModelAttribute + @Valid
@ExceptionHandler(BindException.class)
public ResponseEntity<?> handleBindException(BindException ex) { ... }
// 3. 处理 @Validated 类注解 + 方法参数校验
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<?> handleConstraintViolation(ConstraintViolationException ex) { ... }
// 4. 处理 Spring 6.1+ 默认参数校验(简单类型)
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<?> handleHandlerMethodValidation(HandlerMethodValidationException ex) { ... }
}
五、总结
-
本次异常:
ConstraintViolationException,由 Controller 类上的@Validated触发,针对方法参数中的 JavaBean 及其嵌套属性进行校验。 -
解决方案:添加全局异常捕获,提取
ConstraintViolation中的错误消息。 -
与其他验证异常的关系:与
HandlerMethodValidationException来源不同、触发条件不同,需要分别处理。
全局异常处理器
src/main/java/com/weiyu/exception/GlobalExceptionHandler.java
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j; // Lombok日志注解
import org.springframework.web.bind.annotation.ExceptionHandler; // 异常处理器注解
import org.springframework.web.bind.annotation.RestControllerAdvice; // 控制器增强注解
/**
* 全局异常处理器
* 作用:集中处理整个应用程序中控制器层抛出的异常
*/
@RestControllerAdvice // 组合注解:包含 @ControllerAdvice + @ResponseBody,使返回值自动转为JSON
@SuppressWarnings("unused") // 使用这个注解来抑制警告 或 使用 @Component
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理约束违例异常(ConstraintViolationException)
*
* @param e 捕获的约束违例异常对象
* @return 返回JSON错误信息
*/
@ExceptionHandler(ConstraintViolationException.class)
public Object handleConstraintViolationException(ConstraintViolationException e) {
return Result.error("通过全局异常处理器捕捉到该异常,处理约束违例失败:" + e.getMessage());
}
/**
* 处理所有其他未明确指定的异常(顶级异常处理器)
* 作用:作为异常处理的兜底方案,确保所有异常都被处理
*/
@ExceptionHandler(Exception.class) // 捕获所有未被处理的异常
public Object handleException(Exception e, HttpServletRequest request) {
// ❌ 禁止使用 printStackTrace 在控制台输出异常的详细堆栈跟踪信息
// e.printStackTrace();
// ✅ 规范日志记录:使用日志框架记录完整异常堆栈(参数 e 包含异常的详细堆栈跟踪信息)
log.error("异常错误: {}", e.getMessage(), e); // 记录错误消息和详细堆栈跟踪信息
// 1. 处理文件下载请求的异常
if (ErrorFileResponseUtils.isFileDownloadRequest(request)) {
// 确保错误消息不为空,使用默认消息兜底
String message = StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "下载文件失败";
// 生成包含错误信息的文件响应,使用500状态码
return ErrorFileResponseUtils.createErrorFileResponse(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 2. 普通请求的异常处理
return Result.error(
StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败", // 消息处理
500 // 统一返回500服务器错误状态码
);
}
}
前端:





请完善这里的全局异常处理:
/**
* 处理约束违例异常(ConstraintViolationException)
*
* @param e 捕获的约束违例异常对象
* @return 返回JSON错误信息
*/
@ExceptionHandler(ConstraintViolationException.class)
public Object handleConstraintViolationException(ConstraintViolationException e) {
return Result.error("通过全局异常处理器捕捉到该异常,处理约束违例失败:" + e.getMessage());
}
以下是完善后的 ConstraintViolationException 全局异常处理方法,它会解析所有字段/参数违例详情,并以结构化列表的形式返回,保持与 HandlerMethodValidationException 处理风格一致。
java
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理约束违例异常(ConstraintViolationException)
* 触发场景:
* 1. Controller 类标注 @Validated,方法参数直接标注约束(或参数对象内嵌套验证)
* 2. Service 类标注 @Validated,方法参数/返回值校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolationException(ConstraintViolationException e) {
// 提取所有约束违例的详细错误信息
List<String> errorDetails = e.getConstraintViolations().stream()
.map(this::buildConstraintViolationMessage)
.collect(Collectors.toList());
// 如果解析失败(极少情况),降级返回原始异常信息
if (errorDetails.isEmpty()) {
errorDetails.add(e.getMessage());
}
// 返回统一错误响应,将详细错误列表放入 data 字段
return Result.error("请求参数校验失败", errorDetails);
}
/**
* 将单个 ConstraintViolation 转换为可读的错误消息
* 格式:字段路径 : 错误消息
*/
private String buildConstraintViolationMessage(ConstraintViolation<?> violation) {
// 获取完整的属性路径(如:generateWorkflow.createDTOs[0].deptId)
String propertyPath = violation.getPropertyPath().toString();
// 获取错误消息
String message = violation.getMessage();
// 如果路径为空(如类级别约束),只返回消息
if (propertyPath.isBlank()) {
return message;
}
return propertyPath + ": " + message;
}
}
改进说明
| 改进点 | 实现方式 |
|---|---|
| 提取所有违例详情 | 使用 e.getConstraintViolations() 遍历每个 ConstraintViolation |
| 清晰错误格式 | 字段路径 : 错误消息,路径自动包含嵌套索引(如 createDTOs[0].deptId) |
| 类级别约束处理 | 若 propertyPath 为空,直接返回错误消息,避免显示 : 消息 |
| 降级兜底 | 当违例集合为空(极少见),至少返回 e.getMessage() |
| 统一响应结构 | 与 HandlerMethodValidationException 处理保持对称,使用 Result.error("请求参数校验失败", errorDetails) |
示例响应
json
{
"code": 400,
"msg": "请求参数校验失败",
"data": [
"generateWorkflow.createDTOs[0].deptId: 指标使用部门不能为空",
"generateWorkflow.createDTOs[0].payType: 支出分类不能为空",
"generateWorkflow.createDTOs[1].budget: 预算情况不能为空"
],
"success": false
}
配套建议
-
保持异常处理风格统一:建议将
MethodArgumentNotValidException和BindException也按相同模式处理,所有参数/字段校验错误都返回一致的data错误列表,方便前端统一渲染。 -
参数名保留:若希望 Controller 方法参数名显示在路径中(如
name: 不能为空),请确保编译时使用-parameters选项(Spring Boot 默认开启)。 -
Service 层验证:此处理器同样适用于 Service 层抛出的
ConstraintViolationException,无需额外配置。
修改后的代码:
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j; // Lombok日志注解
import org.springframework.web.bind.annotation.ExceptionHandler; // 异常处理器注解
import org.springframework.web.bind.annotation.RestControllerAdvice; // 控制器增强注解
/**
* 全局异常处理器
* 作用:集中处理整个应用程序中控制器层抛出的异常
*/
@RestControllerAdvice // 组合注解:包含 @ControllerAdvice + @ResponseBody,使返回值自动转为JSON
@SuppressWarnings("unused") // 使用这个注解来抑制警告 或 使用 @Component
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理约束违例异常(ConstraintViolationException)
* 触发场景:
* 1. Controller 类标注 @Validated,方法参数直接标注约束(或参数对象内嵌套验证)
* 2. Service 类标注 @Validated,方法参数/返回值校验失败
*
* @param e 捕获的约束违例异常对象
* @return 返回JSON错误信息
*/
@ExceptionHandler(ConstraintViolationException.class)
public Object handleConstraintViolationException(ConstraintViolationException e) {
// 提取所有约束违例的详细错误信息
List<String> errorDetails = e.getConstraintViolations().stream()
.map(this::buildConstraintViolationMessage)
.collect(Collectors.toList());
// 如果解析失败(极少情况),降级返回原始异常信息
if (errorDetails.isEmpty()) {
errorDetails.add(e.getMessage());
}
// 返回统一错误响应
return Result.error("参数校验失败:" + errorDetails);
}
/**
* 将单个 ConstraintViolation 转换为可读的错误消息
* 格式:字段路径 : 错误消息
*/
private String buildConstraintViolationMessage(ConstraintViolation<?> violation) {
// 获取完整的属性路径(如:generateWorkflow.createDTOs[0].deptId)
String propertyPath = violation.getPropertyPath().toString();
// 获取错误消息
String message = violation.getMessage();
// 如果路径为空(如类级别约束),只返回消息
if (propertyPath.isBlank()) {
return message;
}
return propertyPath + ": " + message;
}
/**
* 处理所有其他未明确指定的异常(顶级异常处理器)
* 作用:作为异常处理的兜底方案,确保所有异常都被处理
*/
@ExceptionHandler(Exception.class) // 捕获所有未被处理的异常
public Object handleException(Exception e, HttpServletRequest request) {
// ❌ 禁止使用 printStackTrace 在控制台输出异常的详细堆栈跟踪信息
// e.printStackTrace();
// ✅ 规范日志记录:使用日志框架记录完整异常堆栈(参数 e 包含异常的详细堆栈跟踪信息)
log.error("异常错误: {}", e.getMessage(), e); // 记录错误消息和详细堆栈跟踪信息
// 1. 处理文件下载请求的异常
if (ErrorFileResponseUtils.isFileDownloadRequest(request)) {
// 确保错误消息不为空,使用默认消息兜底
String message = StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "下载文件失败";
// 生成包含错误信息的文件响应,使用500状态码
return ErrorFileResponseUtils.createErrorFileResponse(message, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 2. 普通请求的异常处理
return Result.error(
StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败", // 消息处理
500 // 统一返回500服务器错误状态码
);
}
}
前端:





更多推荐



所有评论(0)