基于分组与异常实现Spring校验的顺序控制
针对你遇到的分组校验顺序不对、前面组出错仍执行后续校验的问题,结合Hibernate Validator 5.2.4.final的特性,给你一套可以跨项目复用的解决方案:
1. 定义清晰的校验分组接口
先把三个校验阶段拆成独立的分组,方便后续指定校验范围:
// 请求头校验分组 public interface HeaderCheckGroup {} // 请求体通用校验(非空/非空白)分组 public interface RequestBodyBasicCheckGroup {} // 请求体详细校验(格式/长度)分组 public interface RequestBodyDetailCheckGroup {}
2. 配置全局校验序列
创建一个全局序列接口,用@GroupSequence指定严格的执行顺序,这样所有项目都能直接复用这个顺序规则:
import javax.validation.GroupSequence; @GroupSequence({HeaderCheckGroup.class, RequestBodyBasicCheckGroup.class, RequestBodyDetailCheckGroup.class}) public interface OrderedValidationGroup {}
3. 自定义Spring MVC校验处理器
Spring MVC默认的MethodValidationPostProcessor不会按@GroupSequence的顺序中断校验,我们需要自定义这个处理器,让它触发Hibernate Validator的顺序校验逻辑:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; import javax.validation.Validator; import javax.validation.ValidatorFactory; @Configuration public class ValidationConfig { @Bean public MethodValidationPostProcessor methodValidationPostProcessor(ValidatorFactory validatorFactory) { MethodValidationPostProcessor processor = new MethodValidationPostProcessor(); // 绑定支持序列校验的Validator实例 Validator validator = validatorFactory.usingContext().getValidator(); processor.setValidator(validator); return processor; } }
注:Hibernate Validator 5.2.x中,通过ValidatorFactory上下文获取的Validator会严格遵循
@GroupSequence的顺序,前面组校验失败后直接终止,不会执行后续组。
4. Controller层绑定校验规则
- Controller类上标注
@Validated(OrderedValidationGroup.class),指定使用全局校验序列 - 请求头参数的校验注解绑定
HeaderCheckGroup分组:
@RestController @Validated(OrderedValidationGroup.class) public class DemoController { @PostMapping("/submit") public ResponseEntity<String> submit( @RequestHeader @NotBlank(groups = HeaderCheckGroup.class) String token, @RequestHeader @NotBlank(groups = HeaderCheckGroup.class) String appId, @RequestBody @Valid RequestBodyDTO requestBody) { // 业务逻辑 return ResponseEntity.ok("success"); } }
5. 请求体DTO分配校验分组
给请求体的字段按阶段绑定对应分组,级联校验的子对象同样遵循规则:
import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; public class RequestBodyDTO { // 通用校验:非空 @NotBlank(groups = RequestBodyBasicCheckGroup.class) private String userName; // 详细校验:长度+格式 @Size(min = 6, max = 20, groups = RequestBodyDetailCheckGroup.class) @Pattern(regexp = "^[a-zA-Z0-9_]+$", groups = RequestBodyDetailCheckGroup.class) private String password; // 级联校验子对象,自动继承父级的校验序列 @Valid private SubDTO subDTO; // getter/setter } class SubDTO { @NotBlank(groups = RequestBodyBasicCheckGroup.class) private String address; @Pattern(regexp = "^\\d{11}$", groups = RequestBodyDetailCheckGroup.class) private String phone; // getter/setter }
6. 全局异常处理器兼容
你原来的全局异常处理器可以直接复用,因为顺序校验触发的异常(ConstraintViolationException对应请求头,MethodArgumentNotValidException对应请求体)会按顺序抛出,处理器捕获后直接返回错误信息即可:
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { // 处理请求头参数校验异常 @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<List<String>> handleConstraintViolation(ConstraintViolationException e) { List<String> errors = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.toList()); return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } // 处理请求体参数校验异常 @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class}) public ResponseEntity<List<String>> handleMethodArgumentNotValid(MethodArgumentNotValidException e) { List<String> errors = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } }
核心原理说明
之前直接在Controller上加@GroupSequence不生效,是因为Spring MVC默认的校验逻辑会把序列里的所有组一次性传入校验器,没有触发Hibernate Validator的顺序中断机制。自定义处理器后,我们让校验器严格按照@GroupSequence的定义执行,前面组出错就直接终止,完美符合你的需求。
这套方案可以封装成独立的Spring Boot Starter,所有项目直接依赖就能复用校验规则和顺序,扩展性拉满。
内容的提问来源于stack exchange,提问作者sternbergm

