一、为什么需要参数校验?
在日常开发中,参数校验是所有后端接口的起点,也是最容易被忽视的一环。很多系统问题不是因为业务复杂,而是因为”没校验”。例如:用户注册时手机号格式不对、分页接口pageSize传了100000、后台管理新增视频时title为空、金额字段传了负数等。这些问题不仅会让系统变得脆弱,还会增加开发和排查成本。
在企业级项目中,参数校验必须做到:写法统一、提示统一、错误返回结构统一、可扩展可维护、可复用校验逻辑。
二、快速启用参数校验
1. 引入依赖
Spring Boot 2.3+版本需要手动添加验证依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
对于需要更复杂验证逻辑的场合,可能还需额外引入EL表达式支持:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
</dependency>
2. 启用校验
在Controller方法参数前添加@Valid或@Validated注解来触发校验:
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserDTO userDTO) {
// 业务逻辑
return ResponseEntity.ok(userService.create(userDTO));
}
}
三、常用校验注解详解
1. 空值检查注解
| 注解 | 说明 | 支持的数据类型 |
|---|---|---|
| @NotNull | 值不能为null | 任意类型 |
| @NotEmpty | 值不能为null且长度/大小>0 | String、Collection、Map、Array |
| @NotBlank | 值不能为null且必须包含非空白字符 | String |
示例:
public class UserDTO {
@NotNull(message = "用户ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
private String username;
@NotEmpty(message = "角色列表不能为空")
private List<String> roles;
}
2. 数值检查注解
| 注解 | 说明 |
|---|---|
| @Min(value) | 数字最小值 |
| @Max(value) | 数字最大值 |
| @DecimalMin(value) | BigDecimal最小值 |
| @DecimalMax(value) | BigDecimal最大值 |
| @Digits(integer, fraction) | 数字位数限制 |
| @Positive | 正数 |
| @PositiveOrZero | 正数或零 |
| @Negative | 负数 |
| @NegativeOrZero | 负数或零 |
示例:
public class ProductDTO {
@Min(value = 0, message = "价格不能小于0")
private BigDecimal price;
@Digits(integer = 3, fraction = 2, message = "重量格式不正确")
private BigDecimal weight;
@Positive(message = "库存必须为正数")
private Integer stock;
}
3. 字符串检查注解
| 注解 | 说明 |
|---|---|
| @Size(min, max) | 字符串/集合长度范围 |
| @Pattern(regexp) | 正则表达式匹配 |
| 邮箱格式验证 |
示例:
public class UserDTO {
@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;
}
4. 日期检查注解
| 注解 | 说明 |
|---|---|
| @Past | 必须是过去时间 |
| @PastOrPresent | 过去或现在时间 |
| @Future | 必须是未来时间 |
| @FutureOrPresent | 未来或现在时间 |
示例:
public class EventDTO {
@Past(message = "生日必须是过去时间")
private LocalDate birthday;
@Future(message = "开始时间必须是未来时间")
private LocalDateTime startTime;
}
5. 布尔值检查注解
| 注解 | 说明 |
|---|---|
| @AssertTrue | 值必须为true |
| @AssertFalse | 值必须为false |
示例:
public class AgreementDTO {
@AssertTrue(message = "必须接受用户协议")
private Boolean accepted;
}
四、高级用法
1. 分组校验
根据不同场景使用不同的校验规则:
// 定义校验分组
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserDTO {
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空", groups = CreateGroup.class)
private String password;
}
// Controller中使用
@PostMapping("/users")
public ResponseEntity createUser(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {
// 创建用户逻辑
}
@PutMapping("/users")
public ResponseEntity updateUser(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
// 更新用户逻辑
}
2. 嵌套校验
校验对象内部的属性:
public class OrderDTO {
@Valid
@NotNull(message = "用户信息不能为空")
private UserDTO user;
@Valid
@NotEmpty(message = "订单项不能为空")
private List<OrderItemDTO> items;
}
public class OrderItemDTO {
@NotNull(message = "商品ID不能为空")
private Long productId;
@Min(value = 1, message = "数量至少为1")
private Integer quantity;
}
重点:嵌套对象必须加@Valid才能触发递归校验!
3. 列表校验
校验集合中的每个元素:
public class BatchUserDTO {
@NotEmpty(message = "用户列表不能为空")
private List<@Valid UserDTO> users;
}
4. 校验顺序控制
使用@GroupSequence控制校验顺序:
@GroupSequence({First.class, Second.class, UserDTO.class})
public class UserDTO {
@NotBlank(groups = First.class, message = "用户名不能为空")
private String username;
@Email(groups = Second.class, message = "邮箱格式不正确")
private String email;
}
5. 条件校验
使用@AssertTrue进行复杂条件校验:
public class OrderDTO {
private String paymentType;
private String creditCardNumber;
@AssertTrue(message = "信用卡支付需要提供卡号")
public boolean isCreditCardValid() {
if ("CREDIT_CARD".equals(paymentType)) {
return creditCardNumber != null && !creditCardNumber.trim().isEmpty();
}
return true;
}
}
五、自定义校验注解
1. 定义自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
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 boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // 交给@NotNull处理空值
return PHONE_PATTERN.matcher(value).matches();
}
}
3. 使用自定义注解
public class UserDTO {
@Phone
private String phone;
}
六、全局异常处理
1. 统一异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理@RequestBody校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.findFirst()
.orElse("参数校验失败");
return Result.fail(message);
}
// 处理@RequestParam校验异常
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
String message = ex.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElse("参数校验失败");
return Result.fail(message);
}
}
2. 统一返回结果封装
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static Result<Void> success() {
return new Result<>(200, "success", null);
}
public static Result<?> fail(String message) {
return new Result<>(-1, message, null);
}
}
七、最佳实践
1. 校验顺序控制
使用@GroupSequence控制校验顺序,避免不必要的校验:
@GroupSequence({First.class, Second.class, UserDTO.class})
public class UserDTO {
@NotBlank(groups = First.class, message = "用户名不能为空")
private String username;
@Email(groups = Second.class, message = "邮箱格式不正确")
private String email;
}
2. 性能优化建议
- 避免复杂校验逻辑:在校验器中避免数据库查询等耗时操作
- 合理使用分组:减少不必要的校验
- 及时释放资源:在校验器中正确管理资源
- 开启快速失败模式:一旦校验失败就立即返回异常消息
3. 编程式校验
在某些场景下,需要手动触发校验:
@Autowired
private Validator validator;
public void validateRequest(UserDTO userDTO) {
Set<ConstraintViolation<UserDTO>> violations = validator.validate(userDTO);
if (!violations.isEmpty()) {
String errorMessage = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
throw new IllegalArgumentException(errorMessage);
}
}
4. 国际化支持
在resources/ValidationMessages.properties中配置错误消息:
# ValidationMessages.properties
com.example.valid.Phone.message=手机号格式不正确
八、完整示例
1. DTO定义
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, message = "密码至少6位")
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;
@AssertTrue(message = "必须接受协议")
private Boolean agreed;
}
2. Controller层
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@PostMapping
public Result<UserVO> createUser(@Valid @RequestBody UserCreateDTO userDTO) {
// 业务逻辑
return Result.success(userService.create(userDTO));
}
@GetMapping("/{id}")
public Result<UserVO> getUserById(@PathVariable @Min(1) Long id) {
return Result.success(userService.getById(id));
}
}
3. 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.fail(message);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolationException(ConstraintViolationException ex) {
String message = ex.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return Result.fail(message);
}
}
九、总结
Spring Boot参数校验注解提供了强大而灵活的校验机制,能够有效保证接口数据的正确性。通过合理使用各种校验注解、分组校验、自定义校验等特性,可以大大提升代码的健壮性和可维护性。
关键要点:
- 选择合适的注解进行针对性校验
- 使用分组校验应对不同场景需求
- 通过全局异常处理提供统一的错误响应
- 嵌套校验确保复杂对象的完整性
- 自定义注解处理特定业务规则
掌握这些技巧,你将能够构建出更加安全可靠的Spring Boot应用程序。