3-4 参数校验

4 阅读2分钟

3-4 参数校验

概念解析

JSR-303 常用注解

注解说明支持类型
@NotNull不能为 null任意对象
@NotBlank不能为空字符串String
@NotEmpty不能为 null 或空集合Collection/Map/String
@Size长度或大小范围String/Collection/Map/Array
@Length字符串长度范围String
@Range数值范围数字类型
@Min/@Max最小/最大值数字类型
@DecimalMin/@DecimalMax最小/最大值(支持小数)BigDecimal
@Digits整数和小数位数数字类型
@Email邮箱格式String
@Pattern正则表达式String
@URLURL 格式String
@Past过去的时间Date/Calendar
@Future未来的时间Date/Calendar

分组校验

分组说明使用场景
Default默认分组一般验证
Create创建时验证必须字段
Update更新时验证可选字段
自定义分组业务特定分组业务验证

代码示例

1. 基本使用

// 1. 实体类定义校验规则
@Data
public class User {

    @NotNull(message = "用户ID不能为空", groups = {Update.class})
    private Long id;

    @NotBlank(message = "用户名不能为空", groups = {Create.class})
    @Size(min = 2, max = 20, message = "用户名长度在2-20之间")
    private String username;

    @NotBlank(message = "密码不能为空", groups = {Create.class})
    @Size(min = 6, max = 20, message = "密码长度在6-20之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @Min(value = 0, message = "年龄不能小于0")
    @Max(value = 150, message = "年龄不能大于150")
    private Integer age;

    @Past(message = "生日必须是过去的时间")
    private LocalDate birthday;
}

// 2. DTO 定义
@Data
public class CreateUserRequest {

    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, message = "密码至少6位")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;
}

// 3. Controller 使用
@RestController
public class UserController {

    @PostMapping("/users")
    public Result<Void> create(@Valid @RequestBody CreateUserRequest request) {
        // @Valid 触发校验,校验失败抛 MethodArgumentNotValidException
        userService.create(request);
        return Result.success(null);
    }

    @PutMapping("/users/{id}")
    public Result<Void> update(
            @PathVariable Long id,
            @Validated(Update.class) @RequestBody User user) {
        // @Validated(Update.class) 只校验 Update 组的规则
        user.setId(id);
        userService.update(user);
        return Result.success(null);
    }
}

2. 分组定义

// 分组接口
public interface Create extends Default { }

public interface Update extends Default { }

// 或使用 JPA 的 groups
import javax.validation.groups.Default;
import javax.validation.groups.ConvertGroup;
import javax.persistence.GroupSequence;

// 使用分组
@PostMapping
public Result<Void> create(
    @Validated(Create.class) @RequestBody User user) { }

@PutMapping
public Result<Void> update(
    @Validated(Update.class) @RequestBody User user) { }

3. 嵌套校验

@Data
public class OrderRequest {

    @NotNull(message = "用户信息不能为空")
    @Valid  // 嵌套校验
    private User user;

    @NotEmpty(message = "订单项不能为空")
    @Valid
    private List<OrderItem> items;

    @NotNull(message = "收货地址不能为空")
    @Valid
    private Address address;
}

@Data
public class OrderItem {

    @NotNull(message = "商品ID不能为空")
    private Long productId;

    @Min(value = 1, message = "数量至少为1")
    private Integer quantity;
}

@Data
public class Address {

    @NotBlank(message = "收货人不能为空")
    private String receiver;

    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
    private String phone;

    @NotBlank(message = "详细地址不能为空")
    private String detail;
}

4. 自定义校验注解

// 1. 定义注解
@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 {};
}

// 2. 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {

    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) {
        if (value == null || value.isEmpty()) {
            return true;  // 为空由 @NotBlank 处理
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

// 3. 使用自定义注解
@Data
public class UserRequest {

    @Phone
    private String phone;
}

5. 集合参数校验

// 校验 List 中的每个元素
@PostMapping("/users/batch")
public Result<Void> batchCreate(
        @Valid @RequestBody List<CreateUserRequest> requests) {
    // 需要在 Controller 上加 @Validated
    requests.forEach(userService::create);
    return Result.success(null);
}

@Validated  // 需要在 Controller 类上加
@RestController
public class UserController {

    @PostMapping("/users/batch")
    public Result<Void> batchCreate(
            @Valid @RequestBody List<CreateUserRequest> requests) {
        requests.forEach(userService::create);
        return Result.success(null);
    }
}

6. 参数级校验

@RestController
@Validated
public class UserController {

    // 路径参数校验
    @GetMapping("/users/{id}")
    public Result<User> getById(
            @PathVariable @Min(1) Long id) {
        return Result.success(userService.getById(id));
    }

    // 请求参数校验
    @GetMapping("/users/search")
    public Result<List<User>> search(
            @RequestParam(required = false)
            @Size(min = 2, max = 20) String name) {
        return Result.success(userService.search(name));
    }

    // Header 校验
    @GetMapping("/users/export")
    public void export(
            @RequestHeader("Authorization") @NotBlank String token) {
        // ...
    }

    // Cookie 校验
    @GetMapping("/users/profile")
    public Result<User> profile(
            @CookieValue(name = "session_id", required = false) String sessionId) {
        // ...
    }
}

常见坑点

⚠️ 坑 1:嵌套校验不生效

// ❌ 嵌套对象没有 @Valid
@Data
public class OrderRequest {
    private User user;  // 不会校验
}

// ✅ 需要加 @Valid
@Data
public class OrderRequest {

    @Valid
    private User user;

    @Valid
    private List<OrderItem> items;
}

⚠️ 坑 2:分组校验不指定分组

// ❌ 使用 @Valid 不会校验指定分组
@PutMapping
public Result<Void> update(
    @Valid @RequestBody User user) {  // 只校验 Default 组
    userService.update(user);
}

// ✅ 使用 @Validated 指定分组
@PutMapping
public Result<Void> update(
    @Validated(Update.class) @RequestBody User user) {
    userService.update(user);
}

⚠️ 坑 3:日期校验 LocalDateTime

// ❌ @Past/@Future 默认只支持 Date/Calendar
@Past
private LocalDateTime createTime;  // 不生效!

// ✅ 使用 @PastOrPresent / @FutureOrPresent
@PastOrPresent(message = "创建时间必须是过去或现在")
private LocalDateTime createTime;

// 或自定义校验器

⚠️ 坑 4:校验消息国际化

// 配置国际化
@Configuration
public class ValidationConfig {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource source = new ResourceBundleMessageSource();
        source.setBasename("messages");
        source.setDefaultEncoding("UTF-8");
        return source;
    }
}

// 使用 {validatedValue} 获取实际值
@Size(min = 6, message = "密码长度不能少于{min}位,当前${validatedValue.length()}位")

面试题

Q1:@Valid 和 @Validated 的区别?

参考答案

特性@Valid@Validated
来源Bean Validation (JSR-303)Spring
分组校验不支持支持
位置Field/Method/ConstructorClass/Method/Parameter
嵌套校验支持支持
作用范围更广Spring MVC 专用

Q2:校验执行流程?

参考答案

1. 请求到达 Controller
         ↓
2. @Valid/@Validated 触发校验
         ↓
3. Hibernate Validator 执行校验
         ↓
4. 校验失败 → MethodArgumentNotValidException
         ↓
5. GlobalExceptionHandler 捕获
         ↓
6. 返回错误信息

Q3:如何实现密码强度校验?

参考答案

// 方式一:使用多个注解组合
@NotBlank
@Size(min = 8, max = 20, message = "密码长度8-20位")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d).+$",
         message = "密码必须包含字母和数字")
private String password;

// 方式二:自定义注解
@Constraint(validatedBy = PasswordValidator.class)
public @interface StrongPassword {
    String message() default "密码强度不足";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class PasswordValidator implements ConstraintValidator<StrongPassword, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;

        boolean hasLetter = value.matches(".*[A-Za-z]+.*");
        boolean hasDigit = value.matches(".*\\d+.*");
        boolean hasSpecial = value.matches(".*[!@#$%^&*]+.*");
        boolean hasUpper = value.matches(".*[A-Z]+.*");
        boolean hasLower = value.matches(".*[a-z]+.*");

        // 至少 8 位,包含大小写字母和数字
        return hasUpper && hasLower && hasDigit && value.length() >= 8;
    }
}