image

引言

在日常开发中,接口参数校验是保障数据合法性、减少业务异常的关键环节。使用传统的if-else判断不仅代码冗余、可读性差,而且难以维护。Spring Boot 3.x整合Spring Validation(基于JSR-380规范,底层依赖Hibernate Validator 8.x,适配JDK17),提供注解驱动的校验方式,可快速适配单参数、实体类、List列表、嵌套对象等多场景,搭配全局异常处理与自定义校验,轻松搭建标准化校验体系。

本文适配JDK17+SpringBoot3.x环境,覆盖参数校验全场景,重点细化分组校验、完善Service层校验,附完整可复用代码。

一、环境准备

Spring Boot 3.x需显式引入校验starter,依赖适配JDK17的Hibernate Validator 8.x(无需手动指定版本,Spring Boot父工程已管理)。

<!-- 参数校验核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Web支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

注意:Spring Boot 3.x要求Hibernate Validator最低版本为8.x,该版本兼容JDK11+,完美适配JDK17。

二、核心校验注解

掌握常用注解,覆盖绝大多数基础校验场景:

注解 作用 适用类型
@NotNull 非空(仅判断是否为null) 所有引用类型
@NotBlank 非空且去空格后长度>0 String
@NotEmpty 非空且元素个数>0 String、List、数组
@Min(value) 数值不小于指定值 数字类型
@Max(value) 数值不大于指定值 数字类型
@Size(min,max) 长度/个数在指定范围 String、List、数组
@Email 符合邮箱格式 String
@Pattern(regexp) 匹配指定正则表达式 String

三、基础校验场景

3.1 单参数校验(Controller层)

适用于接口参数较少(1-2个)的场景,需在Controller类上添加@Validated注解。

@RestController
@RequestMapping("/user")
@Validated // 开启单参数校验
public class UserController {
    
    // 路径参数校验
    @GetMapping("/{id}")
    public String getUserById(@PathVariable @Min(1) Long id) {
        return "查询用户:" + id;
    }
    
    // 请求参数校验
    @GetMapping("/query")
    public String queryUser(@RequestParam @NotBlank(message = "用户名不能为空") String username) {
        return "查询用户:" + username;
    }
}

3.2 普通实体类校验

适用于接口参数较多的场景,将参数封装为DTO实体类。

步骤1:定义DTO实体类

@Data
public class UserDTO {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度为2-20位")
    private String username;
    
    @NotNull(message = "年龄不能为空")
    @Min(1)
    @Max(150)
    private Integer age;
    
    @Email(message = "邮箱格式错误")
    private String email;
}

步骤2:Controller触发校验

@RestController
@RequestMapping("/user")
public class UserController {
    
    @PostMapping("/create")
    public Result<?> createUser(@RequestBody @Valid UserDTO userDTO) {
        return Result.success("用户创建成功");
    }
}

四、全局异常处理

参数校验失败会抛出异常,需通过全局异常处理器捕获,返回标准化响应。

统一响应类Result

@Data
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;
    
    public static <T> Result<T> success(String msg) {
        Result<T> result = new Result<>();
        result.setCode(HttpStatus.OK.value());
        result.setMsg(msg);
        return result;
    }
    
    public static <T> Result<T> fail(Integer code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
}

全局异常处理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // 处理实体类参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        log.error("参数校验失败:{}", msg);
        return Result.fail(HttpStatus.BAD_REQUEST.value(), msg);
    }
    
    // 处理单参数校验异常
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintException(ConstraintViolationException e) {
        String msg = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(","));
        log.error("参数校验失败:{}", msg);
        return Result.fail(HttpStatus.BAD_REQUEST.value(), msg);
    }
}

五、List列表参数校验

5.1 实体类包含List<对象>

步骤1:定义List元素实体类

@Data
public class ProductDTO {
    @NotBlank(message = "商品名称不能为空")
    @Size(min = 2, max = 50, message = "商品名称长度2-50位")
    private String productName;
    
    @NotNull(message = "商品价格不能为空")
    @Min(value = 1, message = "商品价格不能小于1")
    private BigDecimal price;
}

步骤2:定义包含List的主实体类

@Data
public class OrderDTO {
    @NotBlank(message = "订单号不能为空")
    private String orderNo;
    
    // 核心:@Valid触发List内每个元素的校验
    @Valid
    @NotEmpty(message = "商品列表不能为空")
    @Size(min = 1, max = 10, message = "商品数量不能超过10个")
    private List<ProductDTO> productList;
}

5.2 基础类型List校验

方式1:使用@Pattern

@Data
public class UserBatchDTO {
    @NotBlank(message = "操作人ID不能为空")
    private String operatorId;
    
    // 校验手机号列表
    @Valid
    @NotEmpty(message = "手机号列表不能为空")
    @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式错误")
    private List<String> phoneList;
}

方式2:自定义注解(推荐)

// 自定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneListValidator.class)
public @interface PhoneList {
    String message() default "手机号列表格式错误";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 校验器实现
public class PhoneListValidator implements ConstraintValidator<PhoneList, List<String>> {
    private static final Pattern PHONE_PATTERN = Pattern.compile("1[3-9]\\d{9}");
    
    @Override
    public boolean isValid(List<String> phoneList, ConstraintValidatorContext context) {
        if (phoneList == null || phoneList.isEmpty()) return false;
        
        for (String phone : phoneList) {
            if (phone == null || !PHONE_PATTERN.matcher(phone).matches()) {
                return false;
            }
        }
        return true;
    }
}

// 使用
@Data
public class UserBatchDTO {
    @PhoneList(message = "手机号列表格式错误")
    private List<String> phoneList;
}

六、分组校验

分组校验适用于同一DTO在不同场景下的校验需求。

6.1 定义分组接口

// 公共分组
public interface CommonGroup {}

// 新增分组
public interface AddGroup extends CommonGroup {}

// 更新分组
public interface UpdateGroup extends CommonGroup {}

// 查询分组
public interface QueryGroup {}

6.2 实体类指定分组

@Data
public class UserDTO {
    @Null(groups = AddGroup.class, message = "新增时ID必须为空")
    @Min(value = 1, groups = UpdateGroup.class, message = "更新时ID必须≥1")
    private Long id;
    
    @NotBlank(groups = CommonGroup.class, message = "用户名不能为空")
    @Size(min = 2, max = 20, groups = CommonGroup.class, message = "用户名长度2-20位")
    private String username;
    
    @NotBlank(groups = AddGroup.class, message = "新增时邮箱不能为空")
    private String email;
}

6.3 Controller指定分组

@RestController
@RequestMapping("/user")
public class UserController {
    
    // 新增用户
    @PostMapping("/add")
    public Result<?> addUser(@RequestBody @Validated(AddGroup.class) UserDTO userDTO) {
        return Result.success("用户新增成功");
    }
    
    // 更新用户
    @PutMapping("/update")
    public Result<?> updateUser(@RequestBody @Validated(UpdateGroup.class) UserDTO userDTO) {
        return Result.success("用户更新成功");
    }
}

七、嵌套对象校验

实体类中包含另一个实体类,需在嵌套字段上添加@Valid注解。

@Data
public class OrderDTO {
    @NotBlank(message = "订单号不能为空")
    private String orderNo;
    
    // 嵌套对象校验
    @Valid
    @NotNull(message = "收件人信息不能为空")
    private ReceiverDTO receiver;
}

@Data
public class ReceiverDTO {
    @NotBlank(message = "收件人姓名不能为空")
    private String name;
    
    @Pattern(regexp = "1[3-9]\\d{9}", message = "手机号格式错误")
    private String phone;
}

八、@Valid 与 @Validated 区别

特性 @Valid @Validated
来源 JSR-380标准注解 Spring扩展注解
分组校验 不支持 支持(核心优势)
作用范围 字段、方法参数、嵌套对象 类、方法、参数
嵌套校验 支持(需显式加@Valid) 需配合@Valid触发嵌套

九、Service层校验

9.1 方式1:注入Validator接口(手动校验)

@Service
public class UserService {
    @Autowired
    private Validator validator;
    
    public void saveUser(UserDTO dto) {
        Set<ConstraintViolation<UserDTO>> violations = validator.validate(dto);
        handleValidationResult(violations);
        // 业务逻辑
    }
    
    private void handleValidationResult(Set<ConstraintViolation<UserDTO>> violations) {
        if (!violations.isEmpty()) {
            String errorMsg = violations.stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.joining(","));
            throw new IllegalArgumentException("参数校验失败:" + errorMsg);
        }
    }
}

9.2 方式2:@Validated + 方法参数注解(自动校验)

@Service
@Validated // 开启Service层方法参数的自动校验
public class OrderService {
    
    // 单参数自动校验
    public void checkOrderNo(@NotBlank String orderNo) {
        // 业务逻辑
    }
    
    // 实体类自动校验
    public void saveOrder(@Valid OrderDTO orderDTO) {
        // 业务逻辑
    }
}

十、常见避坑指南

  1. 单参数校验失效:未在Controller/Service类上添加@Validated

  2. 列表元素校验失效:忘记给List字段加@Valid

  3. 嵌套校验失效:嵌套对象字段未加@Valid

  4. 分组校验失效:未用@Validated@Valid不支持分组)

  5. 基础类型List校验错误:直接给List加@NotBlank无效

  6. 版本冲突:手动指定Hibernate Validator版本低于8.x

十一、总结

本文适配JDK17+SpringBoot3.x环境,覆盖参数校验全场景,重点细化分组校验、完善Service层校验。核心流程:引入依赖→添加注解→指定分组→开启校验→全局异常处理→Service层兜底,可适配绝大多数业务场景,提升代码简洁度与接口安全性。