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


原始标题:Validating Lists in a Spring Controller | Baeldung