Spring Boot 参数校验实战:@Valid、分组校验与统一错误响应
2026/7/16 10:13:40 网站建设 项目流程

Spring Boot 参数校验实战:@Valid、分组校验与统一错误响应

写接口时最烦的一件事,就是在 Controller 里手写一堆if (dto.getName() == null || dto.getName().isEmpty())。校验逻辑和业务逻辑搅在一起,同一个字段的规则在新增和更新接口里还要各写一遍。Bean Validation(JSR 380)就是来解决这个问题的,但很多人只会用最基础的@NotNull,一到「新增必填、更新可选」这种场景就抓瞎。这篇把参数校验从入门用到能落地。

朴素写法的问题

先看一个典型的「手写校验」Controller:

@PostMapping("/users")publicResultcreateUser(@RequestBodyUserDTOdto){if(dto.getUsername()==null||dto.getUsername().trim().isEmpty()){returnResult.fail("用户名不能为空");}if(dto.getAge()!=null&&(dto.getAge()<0||dto.getAge()>150)){returnResult.fail("年龄不合法");}if(dto.getEmail()!=null&&!dto.getEmail().contains("@")){returnResult.fail("邮箱格式错误");}// 真正的业务逻辑被埋在下面returnuserService.create(dto);}

问题很明显:校验代码比业务代码还长;换个接口这套还得重写;contains("@")这种手搓的规则也不靠谱。

用注解声明规则

把规则搬到 DTO 字段上,Controller 只留一个@Valid:

publicclassUserDTO{@NotBlank(message="用户名不能为空")// 非 null 且去空格后非空@Size(min=2,max=20,message="用户名长度需 2-20")privateStringusername;@Min(value=0,message="年龄不能为负")@Max(value=150,message="年龄不合法")privateIntegerage;// 包装类型,不填就是 null,跳过校验@Email(message="邮箱格式错误")privateStringemail;// getter/setter 省略}

Controller 加@Valid触发校验:

@PostMapping("/users")publicResultcreateUser(@Valid@RequestBodyUserDTOdto){returnuserService.create(dto);// 校验不过根本进不到这里}

注意@NotBlank@NotEmpty@NotNull的区别:@NotNull只管非 null;@NotEmpty用于集合/字符串要求非空;@NotBlank只用于字符串,还会去掉首尾空格再判断,所以填一堆空格也过不了。

别忘了引依赖(Spring Boot 2.3 之后 web starter 不再自带):

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

分组校验:同一个 DTO,新增和更新规则不同

这是最容易踩坑的场景。新增用户时id必须为空、username必填;更新时id必填、其他字段可选(允许只改一个字段)。用两个 DTO 太重复,正解是校验分组

先定义两个空接口当分组标记:

publicinterfaceCreateGroup{}publicinterfaceUpdateGroup{}

在字段上用groups指定规则属于哪个分组:

publicclassUserDTO{@Null(groups=CreateGroup.class,message="新增时 id 必须为空")@NotNull(groups=UpdateGroup.class,message="更新时 id 不能为空")privateLongid;@NotBlank(groups=CreateGroup.class,message="用户名不能为空")@Size(min=2,max=20,groups={CreateGroup.class,UpdateGroup.class})privateStringusername;}

Controller 里用@Validated指定这次走哪个分组(注意是 Spring 的@Validated,不是@Valid,只有前者支持分组):

@PostMapping("/users")publicResultcreate(@Validated(CreateGroup.class)@RequestBodyUserDTOdto){returnuserService.create(dto);}@PutMapping("/users")publicResultupdate(@Validated(UpdateGroup.class)@RequestBodyUserDTOdto){returnuserService.update(dto);}

同一个字段,新增走「必须为空」,更新走「必须非空」,一份 DTO 搞定。

统一错误响应:别让异常直接抛给前端

校验失败时 Spring 抛MethodArgumentNotValidException,不处理的话前端收到的是一大坨堆栈。用@RestControllerAdvice全局兜住,拼成干净的 JSON:

@RestControllerAdvicepublicclassValidationExceptionHandler{@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)publicResulthandleValidation(MethodArgumentNotValidExceptione){// 把所有字段错误收集成 field -> message 的 mapMap<String,String>errors=newHashMap<>();for(FieldErrorfe:e.getBindingResult().getFieldErrors()){errors.put(fe.getField(),fe.getDefaultMessage());}returnResult.fail("参数校验失败",errors);}}

前端拿到的就是{"code":400,"msg":"参数校验失败","data":{"username":"用户名不能为空"}},能直接定位到哪个字段。

一个进阶坑:嵌套对象和集合要手动加 @Valid

DTO 里套了另一个对象,或者是一个 List,默认不会级联校验。必须在字段上再加@Valid:

publicclassOrderDTO{@NotNull@Valid// 少了这个,address 内部的注解全部失效privateAddressDTOaddress;@NotEmpty@Valid// 校验 list 里每个元素privateList<@ValidItemDTO>items;}

这个坑很隐蔽——外层字段校验正常,内层规则却静默失效,提交脏数据都不报错。凡是嵌套结构,记得逐层补@Valid

小结

  • Controller 只留@Valid/@Validated,规则全部下沉到 DTO 注解,校验与业务解耦。
  • @NotNull(非 null)、@NotEmpty(集合/串非空)、@NotBlank(串去空格非空)三者别混用。
  • 「新增/更新规则不同」用分组校验,配合@Validated(分组.class),一份 DTO 复用。
  • @RestControllerAdvice全局兜住MethodArgumentNotValidException,返回字段级错误 map。
  • 嵌套对象和集合元素要逐层加@Valid才会级联,否则内层规则静默失效。

一句话记忆点:校验规则写在数据上,不写在流程里;分组解决「同一份数据不同场景」,全局处理器解决「错误怎么返回」。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询