Spring Boot 中的 @Valid 和 @Validated 详解:构建健壮的数据校验体系

📅 2025-12-20 23:49:19阅读时间: 48分钟

1. 参数校验概述

在现代化的 Web 应用开发中,参数校验是保障系统健壮性的第一道防线。Spring Boot 通过集成 Bean Validation 规范,为我们提供了强大而灵活的校验机制。其中,@Valid@Validated 是两个核心注解,它们虽然功能相似,但在不同场景下各有优势。

参数校验的主要目的:

  • 数据完整性:确保接收到的数据符合预期格式
  • 业务安全:防止恶意或错误数据进入业务逻辑层
  • 用户体验:前端与后端校验结合,提供即时反馈
  • 系统稳定:减少因数据异常导致的系统崩溃

2. @Valid 和 @Validated 的核心区别

特性 @Valid @Validated
来源 JSR-303/JSR-380 标准 (javax.validation) Spring 框架自定义
分组校验 不支持 支持
嵌套校验 支持 支持
使用场景 方法参数、对象属性 方法参数、类、方法
校验顺序 不支持 支持
校验级别 对象图级别 更细粒度的控制

选择建议

  • 简单对象校验 → 使用 @Valid
  • 需要分组校验 → 使用 @Validated
  • 方法参数校验 → 使用 @Validated

3. 常用校验注解详解

3.1 基础校验注解实战

java 复制代码
public class UserDTO {
    
    @NotNull(message = "ID不能为空")
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
    private String username;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;
    
    @AssertTrue(message = "必须同意协议")
    private Boolean agreed;
    
    @Future(message = "日期必须是未来时间")
    private LocalDateTime appointmentTime;
    
    @DecimalMin(value = "0.0", inclusive = false, message = "金额必须大于0")
    @Digits(integer = 10, fraction = 2, message = "金额格式不正确")
    private BigDecimal amount;
    
    // 构造方法、getter、setter 省略
}

注解说明

  • @NotNull:值不能为null,但可以为空字符串
  • @NotBlank:字符串不能为null且必须包含非空白字符
  • @NotEmpty:集合、数组、字符串不能为null或空
  • @Pattern:使用正则表达式验证字符串格式

3.2 集合与嵌套校验实战

java 复制代码
public class BatchUserDTO {
    
    @NotEmpty(message = "用户列表不能为空")
    @Valid  // 关键:必须添加此注解才能校验集合内元素
    private List<@Valid UserDTO> users; // Java 8+ 写法
    
    // 嵌套对象校验示例
    @Valid
    @NotNull(message = "创建者信息不能为空")
    private UserDTO creator;
    
    // getter/setter
}

4. @Valid 深度使用指南

4.1 Controller 层基础校验

java 复制代码
@RestController
@RequestMapping("/api/users")
@Slf4j
public class UserController {
    
    @PostMapping
    public ResponseEntity<Result<String>> createUser(
            @Valid @RequestBody UserDTO userDTO) {
        // 只有当校验通过时,才会执行到此处的业务逻辑
        log.info("创建用户: {}", userDTO.getUsername());
        return ResponseEntity.ok(Result.success("用户创建成功"));
    }
    
    // 路径变量和查询参数校验需要结合@Validated使用
}

4.2 复杂嵌套对象校验

java 复制代码
public class AddressDTO {
    @NotBlank(message = "省份不能为空")
    private String province;
    
    @NotBlank(message = "城市不能为空")
    private String city;
    
    @NotBlank(message = "详细地址不能为空")
    private String detail;
    
    // getter/setter
}

public class UserDetailDTO {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @Valid  // 关键注解:触发嵌套对象校验
    @NotNull(message = "地址信息不能为空")
    private AddressDTO address;
    
    // 列表嵌套校验
    @Valid
    private List<AddressDTO> addresses;
    
    // getter/setter
}

@PostMapping("/detail")
public ResponseEntity<Result<String>> createUserDetail(
        @Valid @RequestBody UserDetailDTO userDetail) {
    // 处理业务逻辑
    return ResponseEntity.ok(Result.success("用户详情创建成功"));
}

5. @Validated 高级特性实战

5.1 分组校验:按场景精细化控制

java 复制代码
// 定义校验分组接口
public interface CreateGroup {}  // 创建时校验规则
public interface UpdateGroup {}  // 更新时校验规则
public interface QueryGroup {}   // 查询时校验规则

public class UserGroupDTO {
    
    // 更新时ID不能为空,创建时ID必须为空
    @Null(groups = CreateGroup.class, message = "创建时ID必须为空")
    @NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
    private Long id;
    
    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, 
             message = "用户名不能为空")
    @Size(min = 2, max = 20, groups = {CreateGroup.class, UpdateGroup.class})
    private String username;
    
    // 创建时必须提供密码,更新时密码可选
    @NotBlank(groups = CreateGroup.class, message = "密码不能为空")
    @Size(min = 6, groups = CreateGroup.class, message = "密码至少6位")
    private String password;
    
    // getter/setter
}

@RestController
@RequestMapping("/api/group-users")
public class UserGroupController {
    
    @PostMapping
    public ResponseEntity<Result<String>> createUser(
            @Validated(CreateGroup.class) @RequestBody UserGroupDTO userDTO) {
        // 创建用户业务逻辑
        return ResponseEntity.ok(Result.success("用户创建成功"));
    }
    
    @PutMapping
    public ResponseEntity<Result<String>> updateUser(
            @Validated(UpdateGroup.class) @RequestBody UserGroupDTO userDTO) {
        // 更新用户业务逻辑
        return ResponseEntity.ok(Result.success("用户更新成功"));
    }
}

5.2 方法级别参数校验

java 复制代码
@Service
@Validated  // 类级别添加@Validated,开启方法参数校验
@Slf4j
public class UserService {
    
    /**
     * 方法参数校验:基本类型和包装类型
     */
    public UserDTO getUserById(@NotNull(message = "用户ID不能为空") Long id) {
        log.info("查询用户ID: {}", id);
        // 业务逻辑
        return new UserDTO();
    }
    
    /**
     * 方法参数校验:数值范围
     */
    public void updateAge(@Min(0) @Max(150) Integer age) {
        log.info("更新年龄: {}", age);
        // 业务逻辑
    }
    
    /**
     * 多个参数校验
     */
    public List<UserDTO> searchUsers(
            @NotBlank String keyword,
            @Min(1) Integer page,
            @Range(min = 1, max = 100) Integer size) {
        log.info("搜索用户: {}, 页码: {}, 大小: {}", keyword, page, size);
        return new ArrayList<>();
    }
}

@RestController
@RequestMapping("/api/service-users")
public class UserServiceController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity<Result<UserDTO>> getUser(@PathVariable Long id) {
        UserDTO user = userService.getUserById(id);
        return ResponseEntity.ok(Result.success(user));
    }
    
    @GetMapping("/search")
    public ResponseEntity<Result<List<UserDTO>>> searchUsers(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "1") Integer page,
            @RequestParam(defaultValue = "20") Integer size) {
        List<UserDTO> users = userService.searchUsers(keyword, page, size);
        return ResponseEntity.ok(Result.success(users));
    }
}

6. 自定义校验注解实战

6.1 创建手机号校验注解

java 复制代码
/**
 * 手机号校验注解
 */
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {
    
    String message() default "手机号格式不正确";
    
    Class<?>[] groups() default {};
    
    Class<? extends Payload>[] payload() default {};
}

6.2 实现校验逻辑

java 复制代码
/**
 * 手机号校验器
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {
    
    // 手机号正则:1开头,第二位3-9,后面9位数字
    private static final Pattern PHONE_PATTERN = 
        Pattern.compile("^1[3-9]\\d{9}$");
    
    @Override
    public void initialize(Phone constraintAnnotation) {
        // 初始化方法,可以获取注解中的参数
    }
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 为空时跳过校验(结合@NotNull等注解使用)
        if (value == null || value.trim().isEmpty()) {
            return true;
        }
        
        boolean matches = PHONE_PATTERN.matcher(value).matches();
        
        // 自定义错误信息
        if (!matches) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("手机号格式不正确,应为11位数字且以1开头")
                   .addConstraintViolation();
        }
        
        return matches;
    }
}

6.3 使用自定义注解

java 复制代码
public class CustomUserDTO {
    
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @Phone(message = "请输入正确的手机号")
    private String phone;
    
    // 自定义注解支持分组
    @Phone(groups = CreateGroup.class)
    private String contactPhone;
    
    // getter/setter
}

7. 全局异常处理最佳实践

java 复制代码
/**
 * 统一异常处理器
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    /**
     * 处理@RequestBody参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Result<?>> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException ex) {
        
        log.warn("参数校验失败: {}", ex.getMessage());
        
        // 提取详细的字段错误信息
        List<FieldErrorVO> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> new FieldErrorVO(
                    error.getField(),
                    error.getRejectedValue(),
                    error.getDefaultMessage()))
                .collect(Collectors.toList());
        
        Result<List<FieldErrorVO>> result = Result.fail("参数校验失败", errors);
        return ResponseEntity.badRequest().body(result);
    }
    
    /**
     * 处理@RequestParam、@PathVariable参数校验异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<Result<?>> handleConstraintViolationException(
            ConstraintViolationException ex) {
        
        log.warn("参数校验失败: {}", ex.getMessage());
        
        List<FieldErrorVO> errors = ex.getConstraintViolations()
                .stream()
                .map(violation -> {
                    String path = violation.getPropertyPath().toString();
                    // 提取方法参数名
                    String fieldName = path.contains(".") ? 
                        path.substring(path.lastIndexOf(".") + 1) : path;
                    return new FieldErrorVO(
                        fieldName,
                        violation.getInvalidValue(),
                        violation.getMessage());
                })
                .collect(Collectors.toList());
        
        Result<List<FieldErrorVO>> result = Result.fail("参数校验失败", errors);
        return ResponseEntity.badRequest().body(result);
    }
    
    /**
     * 统一返回结果封装
     */
    @Data
    @AllArgsConstructor
    public static class Result<T> {
        private Boolean success;
        private String message;
        private T data;
        private Long timestamp;
        
        public static <T> Result<T> success(T data) {
            return new Result<>(true, "成功", data, System.currentTimeMillis());
        }
        
        public static <T> Result<T> fail(String message, T data) {
            return new Result<>(false, message, data, System.currentTimeMillis());
        }
    }
    
    /**
     * 字段错误信息封装
     */
    @Data
    @AllArgsConstructor
    public static class FieldErrorVO {
        private String field;
        private Object rejectedValue;
        private String message;
    }
}

8. 完整实战示例

8.1 Maven 依赖配置

xml 复制代码
<!-- Spring Boot Web Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<!-- Lombok(可选) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

8.2 综合控制器示例

java 复制代码
@RestController
@RequestMapping("/api/complete-users")
@Validated
@Slf4j
public class CompleteUserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 创建用户 - @RequestBody校验
     */
    @PostMapping
    public ResponseEntity<Result<String>> createUser(
            @Valid @RequestBody UserDTO userDTO) {
        // 业务处理
        userService.createUser(userDTO);
        return ResponseEntity.ok(Result.success("用户创建成功"));
    }
    
    /**
     * 批量创建 - 嵌套集合校验
     */
    @PostMapping("/batch")
    public ResponseEntity<Result<String>> createUsers(
            @Valid @RequestBody BatchUserDTO batchUserDTO) {
        log.info("批量创建用户数量: {}", batchUserDTO.getUsers().size());
        return ResponseEntity.ok(Result.success("批量创建用户成功"));
    }
    
    /**
     * 查询用户 - 方法参数校验
     */
    @GetMapping("/search")
    public ResponseEntity<Result<List<UserDTO>>> searchUsers(
            @RequestParam @NotBlank String keyword,
            @RequestParam @Min(value = 1, message = "页码必须大于0") Integer page,
            @RequestParam @Range(min = 1, max = 100, message = "每页大小1-100") Integer size) {
        
        List<UserDTO> users = userService.searchUsers(keyword, page, size);
        return ResponseEntity.ok(Result.success(users));
    }
    
    /**
     * 分组校验示例
     */
    @PostMapping("/group")
    public ResponseEntity<Result<String>> createUserWithGroup(
            @Validated(CreateGroup.class) @RequestBody UserGroupDTO userDTO) {
        return ResponseEntity.ok(Result.success("分组校验创建成功"));
    }
    
    @PutMapping("/group")
    public ResponseEntity<Result<String>> updateUserWithGroup(
            @Validated(UpdateGroup.class) @RequestBody UserGroupDTO userDTO) {
        return ResponseEntity.ok(Result.success("分组校验更新成功"));
    }
}

9. 高级特性与最佳实践

9.1 校验顺序控制

java 复制代码
public class OrderedUserDTO {
    
    @NotBlank(message = "用户名不能为空", groups = FirstPriority.class)
    private String username;
    
    @Email(message = "邮箱格式不正确", groups = SecondPriority.class)
    private String email;
    
    // 定义校验顺序接口
    public interface FirstPriority {}
    public interface SecondPriority {}
}

// 使用@Validated指定校验顺序
@PostMapping("/ordered")
public ResponseEntity<String> createOrderedUser(
        @Validated({FirstPriority.class, SecondPriority.class}) 
        @RequestBody OrderedUserDTO user) {
    return ResponseEntity.ok("顺序校验成功");
}

9.2 条件性校验

java 复制代码
public class ConditionalUserDTO {
    
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    // 当type为1时,email不能为空
    @AssertTrue(message = "类型为1时必须填写邮箱")
    private boolean isEmailValid() {
        return type != 1 || email != null;
    }
    
    private Integer type;
    private String email;
}

9.3 国际化消息支持

properties 复制代码
# messages.properties
user.id.notnull=用户ID不能为空
user.name.size=用户名长度必须在{min}到{max}之间
java 复制代码
public class I18nUserDTO {
    @NotNull(message = "{user.id.notnull}")
    private Long id;
    
    @Size(min = 2, max = 20, message = "{user.name.size}")
    private String username;
}

10. 性能优化与生产建议

10.1 校验性能优化

java 复制代码
@Configuration
public class ValidationConfig {
    
    /**
     * 自定义验证器,避免重复创建
     */
    @Bean
    public Validator validator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        return factory.getValidator();
    }
}

10.2 生产环境最佳实践

  1. 分层校验策略

    • Controller层:基础格式校验
    • Service层:业务规则校验
    • 数据库层:数据完整性校验
  2. 错误信息设计

    • 前端友好:避免技术性描述
    • 安全考虑:不暴露系统信息
    • 一致性:统一错误格式
  3. 校验粒度控制

    • 创建操作:严格校验
    • 更新操作:选择性校验
    • 查询操作:基础校验

11. 总结与选择指南

通过本文的详细讲解,我们可以看到 Spring Boot 校验体系的强大之处。在实际项目中:

选择 @Valid 的情况

  • 简单的对象图校验
  • 与 JPA 实体校验保持一致性
  • 不需要分组校验的简单场景

选择 @Validated 的情况

  • 需要分组校验(如按业务场景区分)
  • 方法参数校验(如Service层方法)
  • 需要更细粒度控制的复杂场景

核心价值

  • 提高代码健壮性,减少异常处理代码
  • 统一校验逻辑,提高代码可维护性
  • 提升开发效率,减少重复校验代码
  • 改善用户体验,提供即时反馈

掌握 @Valid@Validated 的正确使用,能够让你的 Spring Boot 应用在数据校验方面更加专业和可靠。根据具体业务场景灵活选择,才能发挥最大的效能。