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 生产环境最佳实践
-
分层校验策略:
- Controller层:基础格式校验
- Service层:业务规则校验
- 数据库层:数据完整性校验
-
错误信息设计:
- 前端友好:避免技术性描述
- 安全考虑:不暴露系统信息
- 一致性:统一错误格式
-
校验粒度控制:
- 创建操作:严格校验
- 更新操作:选择性校验
- 查询操作:基础校验
11. 总结与选择指南
通过本文的详细讲解,我们可以看到 Spring Boot 校验体系的强大之处。在实际项目中:
选择 @Valid 的情况:
- 简单的对象图校验
- 与 JPA 实体校验保持一致性
- 不需要分组校验的简单场景
选择 @Validated 的情况:
- 需要分组校验(如按业务场景区分)
- 方法参数校验(如Service层方法)
- 需要更细粒度控制的复杂场景
核心价值:
- 提高代码健壮性,减少异常处理代码
- 统一校验逻辑,提高代码可维护性
- 提升开发效率,减少重复校验代码
- 改善用户体验,提供即时反馈
掌握 @Valid 和 @Validated 的正确使用,能够让你的 Spring Boot 应用在数据校验方面更加专业和可靠。根据具体业务场景灵活选择,才能发挥最大的效能。