参数从哪来
写接口第一步是搞清楚参数从请求的哪个位置取。Spring MVC 提供了一组注解,各管一块:
| 注解 | 数据来源 | 示例 |
|---|---|---|
@PathVariable |
路径中的占位符 | /users/{id} |
@RequestParam |
URL 查询参数或表单字段 | ?page=1 |
@RequestBody |
请求体,通常是 JSON | POST 的 body |
@RequestHeader |
请求头 | Authorization |
@ModelAttribute |
表单字段绑定到对象 | 表单提交 |
GET 请求用 @RequestParam 或 @PathVariable,POST/PUT 传 JSON 用 @RequestBody。一个方法里可以混用,但 @RequestBody 只能有一个,因为请求体只能被读一次。
日期参数建议统一用 yyyy-MM-dd 格式:query 参数在 DTO 字段上加 @DateTimeFormat(pattern = "yyyy-MM-dd"),JSON body 用 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。不指定格式时,前端传 2022-08-08 可能直接报类型转换错误。
数组或集合参数用 @RequestParam List<Long> ids,请求写成 ?ids=1&ids=2,或者 ?ids=1,2 也能绑定。参数个数不固定时不要写一堆方法重载,直接收集合更灵活。
表单提交和 JSON 提交的校验行为不完全一样:表单对象校验失败抛 BindException,JSON body 抛 MethodArgumentNotValidException,全局异常处理里两种都要覆盖,否则总有一边返回默认错误页。
加校验注解
手工写 if (name == null || name.trim().isEmpty()) 既啰嗦又容易漏。引入校验依赖后,直接在 DTO 字段上写注解:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注意 Spring Boot 2.3 之后校验依赖不再默认包含,只加 starter-web 时,要么编译时报找不到注解,要么校验器缺失导致注解不生效。
@Valid 加在方法参数上,校验的是这个参数对象内部的约束;@Validated 加在类上,才会启用对 @RequestParam、@PathVariable 这类方法参数的校验。两者别混。升级 Spring Boot 大版本时,校验包的命名空间会从 javax 变成 jakarta,依赖也要跟着换。
常用的几个注解:
| 注解 | 作用 |
|---|---|
@NotNull |
不能为 null,可以是空字符串 |
@NotBlank |
字符串不能为 null、不能全是空白 |
@NotEmpty |
集合/字符串不能为 null 且长度大于 0 |
@Size(min, max) |
长度范围 |
@Min / @Max |
数值范围 |
@Email |
邮箱格式 |
@Pattern |
正则匹配 |
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(max = 20, message = "用户名不能超过 20 个字符")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄必须大于 0")
private Integer age;
// 省略 getter / setter
}
@Valid 与 @Validated
两个注解都能触发校验,区别在于:
@Valid来自 JSR-303,标准注解,用在参数、字段、方法上;@Validated是 Spring 提供的,额外支持分组,还可以标在类上开启方法参数校验。
最常用的组合是:Controller 方法的 @RequestBody 参数前加 @Valid,类上加 @Validated,后者主要是为了 @RequestParam、@PathVariable 上的约束能生效。
嵌套对象要特别注意:UserCreateDTO 里如果还有一个 AddressDTO 字段,只在参数上加 @Valid 不会校验 AddressDTO 的内部字段,必须在字段上再标一个 @Valid:
public class UserCreateDTO {
@Valid
@NotNull(message = "地址不能为空")
private AddressDTO address;
}
集合元素同理:List<@Valid AddressDTO> addresses 或字段上的 @Valid,光靠外层参数上的 @Valid 管不到里面。
分组校验
新增和修改往往用同一个 DTO:新增时 id 必须为空,修改时 id 必须不为空。这时用分组:
public interface Create {
}
public interface Update {
}
public class UserDTO {
@Null(groups = Create.class, message = "新增时不能指定 id")
@NotNull(groups = Update.class, message = "修改时必须指定 id")
private Long id;
@NotBlank(groups = {Create.class, Update.class}, message = "用户名不能为空")
private String name;
}
@PostMapping
public void create(@RequestBody @Validated(Create.class) UserDTO dto) {
}
@PutMapping
public void update(@RequestBody @Validated(Update.class) UserDTO dto) {
}
分组接口就是空接口,用来当标记。不写 groups 的注解属于 Default 分组,指定了分组之后默认分组就不再校验,这一点容易踩。
常见坑
1. 校验注解完全不生效
先看依赖里有没有 spring-boot-starter-validation,再看参数前有没有 @Valid 或 @Validated。两个都没问题,再检查是不是捕获了 MethodArgumentNotValidException 却什么都没做。
2. @RequestParam 上的约束不生效
@RequestParam @Min(1) Integer page 这种写法,光有 @Valid 不够,Controller 类上必须加 @Validated,否则约束根本不会被执行。
3. 嵌套对象没加 @Valid
外层字段校验通过,内层却传了空对象也能成功。给嵌套字段补上 @Valid,别只在外层参数上标一个。
4. javax 和 jakarta 混用
Spring Boot 2.7 用的是 javax.validation.constraints.*,Spring Boot 3.0 换成了 jakarta.validation.constraints.*。从旧项目复制代码到 3.x 报“找不到包”,就是这里的原因。
5. 校验失败返回默认 400
不写全局异常处理时,校验失败返回的是 Spring 默认的错误 JSON,字段名和提示都很别扭。用 @RestControllerAdvice 统一处理 MethodArgumentNotValidException,把错误信息整理成业务统一格式。
排查步骤
- 确认
spring-boot-starter-validation在依赖树里:mvn dependency:tree -Dincludes=*validation*; - 确认参数前有
@Valid/@Validated,并且类上标了@Validated; - 在全局异常处理器里打印
BindingResult的字段错误,确认校验到底跑没跑; - 用 Postman 或 curl 构造边界数据,逐条验证注解的 message;
- 检查
javax与jakarta是否和当前 Spring Boot 版本匹配。
评论