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 | 整数和小数位数 | 数字类型 |
| 邮箱格式 | String | |
| @Pattern | 正则表达式 | String |
| @URL | URL 格式 | 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/Constructor | Class/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;
}
}