1. 简介
接口参数校验是每个后端服务绕不开的环节。本文聚焦一个高频但容易踩坑的场景:如何在 Spring 的 Controller 中对传入的 List
类型参数进行有效校验。
我们将在 Controller 层完成校验逻辑,确保前端传来的数据符合业务规则。这种做法既简洁又能快速失败(fail-fast),避免脏数据进入服务深处。
2. 为对象字段添加约束
我们以电影管理系统为例。假设有一个批量添加电影的接口,接收一个电影列表。首先,定义 Movie
实体类,并使用 JSR-380(即 Bean Validation)注解 对字段进行约束。
✅ 正确姿势:使用标准注解明确字段规则
public class Movie {
private String id;
@NotEmpty(message = "Movie name cannot be empty.")
private String name;
// standard setters and getters
}
⚠️ 注意:@NotEmpty
会同时校验 null
和空字符串,比 @NotBlank
更适合此处场景。
3. 在 Controller 中启用校验
3.1 启用方法级校验
要在 Controller 方法参数上使用校验注解,必须先在类上添加 @Validated
—— 这是 Spring 提供的方法级别校验支持,由 MethodValidationPostProcessor
驱动。
❌ 踩坑提醒:只加 @Valid
而不加 @Validated
是无效的!
@Validated
@RestController
@RequestMapping("/movies")
public class MovieController {
@Autowired
private MovieService movieService;
//...
}
3.2 校验 List 自身及其元素
现在编写核心接口方法。我们需要两个层面的校验:
- ✅ List 本身不能为空(
@NotEmpty
) - ✅ 每个元素都必须满足
Movie
的字段约束(@Valid
)
@PostMapping
public void addAll(
@RequestBody
@NotEmpty(message = "Input movie list cannot be empty.")
List<@Valid Movie> movies) {
movieService.addAll(movies);
}
📌 关键语法说明:
List<@Valid Movie>
:这是 Java 8 引入的 类型注解(Type Annotation),表示泛型内部的每个Movie
实例都要被@Valid
触发校验。@Valid
放在泛型里才能穿透到每个元素,否则只校验 List 引用本身(毫无意义)。
3.3 验证效果
输入情况 | 抛出异常 | 返回消息 |
---|---|---|
空数组 [] |
ConstraintViolationException |
Input movie list cannot be empty. |
包含 name 为空的 Movie | ConstraintViolationException |
Movie name cannot be empty. |
4. 自定义 List 级别校验器
Spring 内置注解无法满足所有业务需求。比如我们希望限制一次最多添加 4 部电影,这就需要自定义约束。
4.1 定义自定义注解
@Constraint(validatedBy = MaxSizeConstraintValidator.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxSizeConstraint {
String message() default "The input list cannot contain more than 4 movies.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
4.2 实现校验逻辑
public class MaxSizeConstraintValidator
implements ConstraintValidator<MaxSizeConstraint, List<Movie>> {
@Override
public boolean isValid(List<Movie> values, ConstraintValidatorContext context) {
return values != null && values.size() <= 4;
}
}
⚠️ 注意事项:
- 第二个泛型必须是
List<Movie>
,不能是List<?>
,否则泛型擦除会导致运行时类型丢失。 - 手动判空是必要的,因为即使有
@NotEmpty
,也建议在自定义校验器中保持独立性和健壮性。
4.3 应用到 Controller
@PostMapping
public void addAll(
@RequestBody
@NotEmpty(message = "Input movie list cannot be empty.")
@MaxSizeConstraint
List<@Valid Movie> movies) {
movieService.addAll(movies);
}
现在如果传入 5 个电影对象,就会触发自定义校验失败。
5. 统一异常处理
当任意校验失败时,Spring 会抛出 ConstraintViolationException
。我们需要一个全局异常处理器来友好地返回错误信息。
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handle(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
String errorMessage = violations.stream()
.map(ConstraintViolation::getMessage)
.reduce((a, b) -> a + ";" + b)
.orElse("Invalid request.");
return ResponseEntity.badRequest().body(errorMessage);
}
✅ 改进建议:
- 使用
Stream
替代StringBuilder
更简洁现代 - 多个错误用分号分隔,便于前端解析
- 返回
400 Bad Request
状态码符合 REST 规范
6. 接口测试验证
6.1 正常请求(200)
curl -v -d '[{"name":"Inception"}]' \
-H "Content-Type: application/json" \
-X POST http://localhost:8080/movies
✅ 响应:
HTTP/1.1 200 OK
6.2 空列表(400)
curl -d '[]' \
-H "Content-Type: application/json" \
-X POST http://localhost:8080/movies
❌ 响应:
Input movie list cannot be empty.
6.3 超过数量限制(400)
curl -d '[{"name":"A"},{"name":"B"},{"name":"C"},{"name":"D"},{"name":"E"}]' \
-H "Content-Type: application/json" \
-X POST http://localhost:8080/movies
❌ 响应:
The input list cannot contain more than 4 movies.
7. 总结
本文系统性地演示了如何在 Spring MVC 中安全、有效地校验 List 类型参数:
- ✅ 使用
@Validated
开启方法校验 - ✅ 利用
List<@Valid T>
实现元素级穿透校验 - ✅ 自定义
@Constraint
处理复杂业务规则 - ✅ 全局异常捕获提升 API 友好性
这些技巧在实际项目中非常实用,尤其适合批量操作接口。掌握它们能让你的代码更健壮,减少低级数据错误。
示例完整代码见 GitHub:https://github.com/baeldung/spring-mvc-basics/tree/master