SpringValidation参数校验

SpringValidation参数校验

Spring Validation 基于 JSR-380(Bean Validation 2.0),提供了一套完整的参数校验机制。

引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

常用校验注解

空值检查

注解 说明
@Null 必须为 null
@NotNull 不能为 null
@NotBlank 不能为 null 且至少一个非空白字符
@NotEmpty 不能为 null 且不为空(字符串/集合/数组)

数值检查

注解 说明
@Min 最小值
@Max 最大值
@Range 范围(@Min + @Max)
@DecimalMin 最小小数
@DecimalMax 最大小数
@Positive 正数
@PositiveOrZero 正数或零
@Negative 负数
@NegativeOrZero 负数或零
@Digits 小数位数限制

字符串检查

注解 说明
@Size 长度/大小范围
@Length 长度范围(Hibernate)
@Pattern 正则匹配
@Email 邮箱格式
@URL URL 格式(Hibernate)
@CreditCardNumber 信用卡号(Hibernate)

时间检查

注解 说明
@Future 未来时间
@FutureOrPresent 未来或现在
@Past 过去时间
@PastOrPresent 过去或现在

基本使用

实体校验

@Data
public class UserCreateRequest {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少6位")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d).+$",
message = "密码必须包含字母和数字")
private String password;

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

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

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

控制器使用

@RestController
@RequestMapping("/users")
public class UserController {

@PostMapping
public Result<Long> create(@Valid @RequestBody UserCreateRequest request) {
return Result.success(userService.create(request));
}

@PutMapping("/{id}")
public Result<Void> update(@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
userService.update(id, request);
return Result.success();
}
}

分组校验

public interface CreateGroup {}
public interface UpdateGroup {}
public interface DeleteGroup {}

@Data
public class UserRequest {

@NotNull(groups = UpdateGroup.class, message = "ID不能为空")
private Long id;

@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
@Size(min = 2, max = 20, groups = {CreateGroup.class, UpdateGroup.class})
private String username;

@NotBlank(groups = CreateGroup.class)
@Size(min = 6, groups = CreateGroup.class)
private String password;

@NotBlank(groups = {CreateGroup.class, UpdateGroup.class})
@Email(groups = {CreateGroup.class, UpdateGroup.class})
private String email;
}
@RestController
public class UserController {

@PostMapping("/users")
public Result<Long> create(
@Validated(CreateGroup.class) @RequestBody UserRequest request) {
return Result.success(userService.create(request));
}

@PutMapping("/users/{id}")
public Result<Void> update(
@Validated(UpdateGroup.class) @RequestBody UserRequest request) {
userService.update(request);
return Result.success();
}
}

嵌套校验

@Data
public class OrderCreateRequest {

@NotBlank(message = "订单编号不能为空")
private String orderNo;

@NotNull(message = "用户ID不能为空")
private Long userId;

@Valid // 嵌套校验
@NotEmpty(message = "订单项不能为空")
private List<OrderItemRequest> items;

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

@Data
public class OrderItemRequest {
@NotBlank(message = "SKU不能为空")
private String skuId;

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

@DecimalMin(value = "0.01", message = "单价必须大于0")
private BigDecimal price;
}

@Data
public class AddressRequest {
@NotBlank(message = "收件人不能为空")
private String receiver;

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

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

自定义校验注解

手机号校验

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

public class PhoneValidator implements ConstraintValidator<Phone, String> {

private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // @NotNull/@NotBlank 处理空值
}
return PHONE_PATTERN.matcher(value).matches();
}
}

枚举值校验

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {
String message() default "枚举值不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}

public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {

private Set<String> values;

@Override
public void initialize(EnumValue annotation) {
values = Arrays.stream(annotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}

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

使用自定义注解

@Data
public class UserRequest {
@Phone
private String phone;

@EnumValue(enumClass = UserStatus.class, message = "用户状态不正确")
private String status;
}

public enum UserStatus {
ACTIVE, INACTIVE, DELETED
}

手动触发校验

@Service
public class UserService {

@Autowired
private Validator validator;

public void createUser(UserCreateRequest request) {
// 手动校验
Set<ConstraintViolation<UserCreateRequest>> violations =
validator.validate(request);

if (!violations.isEmpty()) {
String message = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
throw new ValidationException(message);
}

// 业务逻辑
}

// 校验单个属性
public void validateUsername(String username) {
Set<ConstraintViolation<UserCreateRequest>> violations =
validator.validateValue(UserCreateRequest.class, "username", username);
// ...
}

// 分组校验
public void updateUser(UserRequest request) {
Set<ConstraintViolation<UserRequest>> violations =
validator.validate(request, UpdateGroup.class);
// ...
}
}

全局异常处理

@RestControllerAdvice
public class ValidationExceptionHandler {

@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.fail(400, message);
}

@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolation(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return Result.fail(400, message);
}
}

总结

Spring Validation 提供了优雅的参数校验方案:

  1. 注解驱动:声明式校验,无侵入
  2. 分组校验:不同场景使用不同规则
  3. 嵌套校验:支持复杂对象结构
  4. 自定义扩展:满足业务特定规则
  5. 手动触发:灵活控制校验时机

合理使用参数校验,可以在入口层就拦截非法数据,减少后续业务逻辑的防御性代码。


   转载规则


《SpringValidation参数校验》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录