Spring Validation作为Spring生态系统的重要组成部分,提供了一套强大而灵活的数据校验机制。
1. Bean Validation基础注解
Spring Validation集成了JSR-380 (Bean Validation 2.0)规范,提供了一系列开箱即用的校验注解。
常用注解示例
@Data
public class UserDTO {@NotNull(message = "用户ID不能为空")private Long id;@NotBlank(message = "用户名不能为空")@Size(min = 4, max = 20, message = "用户名长度必须在4到20个字符之间")private String username;@Email(message = "邮箱格式不正确")private String email;@Min(value = 18, message = "年龄必须大于或等于18")@Max(value = 120, message = "年龄必须小于或等于120")private Integer age;@Past(message = "出生日期必须是过去的日期")private LocalDate birthDate;@Pattern(regexp = "^1[3-9]\d{9}$", message = "手机号码格式不正确")private String phoneNumber;
}
在控制器中应用
@RestController
@RequestMapping("/api/users")
public class UserController {@PostMappingpublic ResponseEntity<UserDTO> createUser(@RequestBody @Valid UserDTO userDTO, BindingResult bindingResult) {if (bindingResult.hasErrors()) {// 处理验证错误throw new ValidationException(bindingResult);}// 处理业务逻辑return ResponseEntity.ok(userDTO);}
}
最佳实践:使用有意义的错误消息,保持一致的命名风格,避免在实体类上直接使用验证注解,而是在DTO对象上应用验证规则。
2. 自定义约束验证器
Spring Validation允许开发者创建自定义约束,满足特定业务规则的验证需求。
定义自定义约束注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueUsernameValidator.class)
public @interface UniqueUsername {String message() default "用户名已存在";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}
实现验证器
public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {@Autowiredprivate UserRepository userRepository;@Overridepublic boolean isValid(String username, ConstraintValidatorContext context) {if (username == null) {return true; // 让@NotNull处理空值}return !userRepository.existsByUsername(username);}
}
应用自定义约束
public class UserRegistrationDTO {@NotBlank@Size(min = 4, max = 20)@UniqueUsernameprivate String username;// 其他字段...
}
使用场景:验证业务特定规则,如唯一性约束、密码复杂度、信用卡格式等。
3. 分组验证
分组验证允许根据不同场景应用不同的验证规则,例如创建和更新操作可能需要不同的验证逻辑。
定义验证分组
// 定义验证分组接口
public interface ValidationGroups {interface Create {}interface Update {}
}
应用分组到约束
@Data
public class ProductDTO {@Null(groups = ValidationGroups.Create.class, message = "创建产品时ID必须为空")@NotNull(groups = ValidationGroups.Update.class, message = "更新产品时ID不能为空")private Long id;@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class})private String name;@PositiveOrZero(groups = ValidationGroups.Create.class)@Positive(groups = ValidationGroups.Update.class)private BigDecimal price;
}
在控制器中指定分组
@RestController
@RequestMapping("/api/products")
public class ProductController {@PostMappingpublic ResponseEntity<ProductDTO> createProduct(@RequestBody @Validated(ValidationGroups.Create.class) ProductDTO productDTO) {// 创建产品逻辑return ResponseEntity.ok(productDTO);}@PutMapping("/{id}")public ResponseEntity<ProductDTO> updateProduct(@PathVariable Long id,@RequestBody @Validated(ValidationGroups.Update.class) ProductDTO productDTO) {// 更新产品逻辑return ResponseEntity.ok(productDTO);}
}
提示:注意使用@Validated
注解而不是@Valid
,因为只有前者支持分组验证。
4. 嵌套验证
嵌套验证允许验证复杂对象结构中的嵌套对象。
定义嵌套对象
@Data
public class OrderDTO {@NotNullprivate Long id;@NotNull@Valid // 标记需要级联验证的字段private CustomerDTO customer;@NotEmpty@Valid // 验证集合中的每个元素private List<OrderItemDTO> items;
}@Data
public class CustomerDTO {@NotNullprivate Long id;@NotBlankprivate String name;@Emailprivate String email;@Valid // 进一步嵌套验证private AddressDTO address;
}
关键点:在需要级联验证的字段上添加@Valid
注解,确保验证深入到嵌套对象中。
5. 方法级别验证
Spring Validation不仅可以用于控制器参数,还可以应用于服务层的方法。
启用方法级别验证
@Configuration
@EnableMethodValidation
public class ValidationConfig {// 配置内容
}
定义带验证的服务方法
@Service
public class UserService {@Validatedpublic User createUser(@Valid UserDTO userDTO) {// 业务逻辑return new User();}@NotNullpublic User findById(@Min(1) Long id) {// 查询逻辑return new User();}@Validated(ValidationGroups.Update.class)public void updateUser(@Valid UserDTO userDTO) {// 更新逻辑}
}
应用场景:确保服务层方法接收到的参数和返回的结果符合预期,增强代码的健壮性。
6. 错误消息处理和国际化
Spring Validation提供了强大的错误消息处理和国际化支持。
自定义错误消息
在ValidationMessages.properties
文件中定义:
# ValidationMessages.properties
javax.validation.constraints.NotEmpty.message=字段不能为空
javax.validation.constraints.Email.message=不是有效的电子邮箱地址
user.name.size=用户名长度必须在{min}到{max}个字符之间
国际化错误消息
创建特定语言的属性文件:
# ValidationMessages_en.properties
javax.validation.constraints.NotEmpty.message=Field cannot be empty
javax.validation.constraints.Email.message=Not a valid email address
user.name.size=Username must be between {min} and {max} characters# ValidationMessages_zh_CN.properties
javax.validation.constraints.NotEmpty.message=字段不能为空
javax.validation.constraints.Email.message=不是有效的电子邮箱地址
user.name.size=用户名长度必须在{min}到{max}个字符之间
使用自定义消息
@Size(min = 4, max = 20, message = "{user.name.size}")
private String username;
7. 程序化验证
除了注解驱动的验证,Spring Validation还支持以编程方式进行验证。
使用Validator手动验证对象
@Service
public class ValidationService {private final Validator validator;public ValidationService(Validator validator) {this.validator = validator;}public <T> void validate(T object) {Set<ConstraintViolation<T>> violations = validator.validate(object);if (!violations.isEmpty()) {throw new ConstraintViolationException(violations);}}public <T> void validateWithGroup(T object, Class<?>... groups) {Set<ConstraintViolation<T>> violations = validator.validate(object, groups);if (!violations.isEmpty()) {throw new ConstraintViolationException(violations);}}public <T> List<String> getValidationErrors(T object) {return validator.validate(object).stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());}
}
使用场景:在复杂业务逻辑中需要条件性验证,或者验证非控制器传入的对象时。
8. 组合约束
组合约束允许将多个基本约束组合成一个更复杂的约束,减少代码重复。
创建组合约束
@NotNull
@Size(min = 8, max = 30)
@Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$", message = "密码必须包含至少一个数字、小写字母、大写字母和特殊字符")
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface StrongPassword {String message() default "密码不符合安全要求";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}
应用组合约束
public class PasswordChangeDTO {@NotBlankprivate String oldPassword;@StrongPasswordprivate String newPassword;@NotBlankprivate String confirmPassword;
}
优点:提高代码可读性和可维护性,确保验证规则在整个应用中保持一致。
9. 跨字段验证
跨字段验证允许根据多个字段之间的关系进行验证。
创建类级别约束
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
public @interface PasswordMatches {String message() default "确认密码与新密码不匹配";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};String field();String fieldMatch();
}
实现验证器
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {private String field;private String fieldMatch;@Overridepublic void initialize(PasswordMatches constraintAnnotation) {this.field = constraintAnnotation.field();this.fieldMatch = constraintAnnotation.fieldMatch();}@Overridepublic boolean isValid(Object value, ConstraintValidatorContext context) {try {Object fieldValue = BeanUtils.getPropertyDescriptor(value.getClass(), field).getReadMethod().invoke(value);Object fieldMatchValue = BeanUtils.getPropertyDescriptor(value.getClass(), fieldMatch).getReadMethod().invoke(value);return (fieldValue != null) && fieldValue.equals(fieldMatchValue);} catch (Exception e) {return false;}}
}
应用类级别约束
@Data
@PasswordMatches(field = "newPassword", fieldMatch = "confirmPassword")
public class PasswordChangeDTO {@NotBlankprivate String oldPassword;@StrongPasswordprivate String newPassword;@NotBlankprivate String confirmPassword;
}
使用场景:验证密码确认、日期范围比较、最小/最大值比较等。
总结
Spring Validation提供了一套全面而强大的数据校验工具,从基本的注解验证到复杂的自定义约束,从单一字段验证到跨字段关系验证,都有相应的解决方案。
合理利用这些验证工具,不仅能提高应用的健壮性和安全性,还能改善代码质量和可维护性。