引言
在日常开发中,接口参数校验是保障数据合法性、减少业务异常的关键环节。使用传统的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) {
// 业务逻辑
}
}
十、常见避坑指南
-
单参数校验失效:未在Controller/Service类上添加
@Validated -
列表元素校验失效:忘记给List字段加
@Valid -
嵌套校验失效:嵌套对象字段未加
@Valid -
分组校验失效:未用
@Validated(@Valid不支持分组) -
基础类型List校验错误:直接给List加
@NotBlank无效 -
版本冲突:手动指定Hibernate Validator版本低于8.x
十一、总结
本文适配JDK17+SpringBoot3.x环境,覆盖参数校验全场景,重点细化分组校验、完善Service层校验。核心流程:引入依赖→添加注解→指定分组→开启校验→全局异常处理→Service层兜底,可适配绝大多数业务场景,提升代码简洁度与接口安全性。

