SpringBoot参数验证高阶技巧
1 基本注解
Spring Validation 提供了一组用于常见验证任务的标准如下示例:

在Controller接口参数上开启验证功能:

2 自定义注解验证
对于特定的业务规则,你可以创建自己的自定义验证约束。

public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, String> {
@Resource
private UserRepository userRepository;
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
if (username == null) {
return true; // Let @NotNull handle null values
}
return !userRepository.existsByUsername(username);
}
}3 分组验证
分组验证允许你针对不同场景应用不同的规则,例如创建操作与更新操作。如下示例(定义分组):

使用分组
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;
}分组验证

注意:使用 @Validated 注解,因为它是支持验证组的注解。**@Valid** 不支持。
4 嵌套验证
对于复杂对象,你可以验证嵌套对象和集合。

注意:使用 @Valid 注解需要级联验证的字段,以确保验证器深入检查嵌套对象。
5 方法级验证
你也可以直接对服务层方法进行验证,而不仅仅是对控制器参数进行验证。

如果你是在Controller方法上那么你不需要在类上使用@Validated注解。
6 错误消息国际化
当发生错误时我们可以通过2中方式来提示具体的错误消息。
直接指定错误消息

直接通过 message 指定要展示的错误消息。

- 使用占位符
message属性可以使用 {xxx} 语法,其中 xxx 为你在资源文件中定义的key。

接下来,新建 classpath:messages_zh_CN.properties 和 classpath:messages_en_US.properties 资源文件。

上面我们是使用的spring boot默认的basename,你可以自定义,通过 spring.messages.basename 属性配置。
7 编程验证
除了注解之外,你还可以手动触发验证。

适用于验证是有条件的复杂业务逻辑,或用于验证未通过控制器传递的对象。
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<? extenjavads Payload>[] payload() default {};
}使用组合注解

优点:提高代码可读性,并确保验证规则得到一致应用。
9 跨字段验证
根据字段与其他字段的关系来验证字段,例如确保 “确认密码” 字段与 “密码” 字段匹配。创建一个类级约束:

密码验证器
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
private String field;
private String fieldMatch;
@Override
public void initialize(PasswordMatches constraintAnnotation) {
this.field = constraintAnnotation.field();
this.fieldMatch = constraintAnnotation.fieldMatch();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
try {
// 通过反射获取字段值
Field field1 = value.getClass().getDeclaredField(field);
field1.setAccessible(true);
Object fieldValue1 = field1.get(value);
Field field2 = value.getClass().getDeclaredField(fieldMatch);
field2.setAccessible(true);
Object fieldValue2 = field2.get(value);
// 比较两个字段值(处理null情况)
if (fieldValue1 == null) {
return fieldValue2 == njavaull;
}
return fieldValue1.equals(fieldValue2);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("Failed to validate password fields", e);
}
}
}使用注解

Controller接口
@PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody UserRegistrationDTO dto, BindingResult result) {
if (result.hasErrors()) {
List<String> errors = result.getAllErrors()
.stream()
.map(err -> err.getDefaultMessage())
.toList() ;
return ResponseEntity.badRequest().body(errors);
}
return ResponseEntity.ok("Registration successful");
}