SpringBoot参数校验全攻略:从入门到企业级实践

一、为什么需要参数校验?

在日常开发中,参数校验是所有后端接口的起点,也是最容易被忽视的一环。很多系统问题不是因为业务复杂,而是因为”没校验”。例如:用户注册时手机号格式不对、分页接口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且长度/大小>0String、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)正则表达式匹配
@Email邮箱格式验证

示例:

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应用程序。


作 者:南烛
链 接:https://www.itnotes.top/archives/1076
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇