前言
在 API 开发中,参数校验是数据流转的第一道防线。非法参数不仅可能导致业务逻辑异常,更可能引发系统安全问题(如 SQL 注入、数据溢出等)。Hibernate Validator 具备轻量集成、注解驱动、可扩展等优势,已成为 Spring Boot 项目的首选参数校验方案。
本文基于实际项目开发经验,从「基础用法-进阶实践-问题排查-规范落地」四个维度,提供可直接复用的实战指南。
核心目标:规范接口参数校验逻辑,保障数据合法性,降低系统异常风险 。
1. 环境准备与依赖说明
1.1 核心依赖
Spring Boot 提供 spring-boot-starter-validation Starter,内置 Hibernate Validator 及适配依赖,无需额外引入其他 Jar 包。
Maven 依赖配置:
<!-- 参数校验核心依赖(Spring Boot 2.3+ 需显式引入) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version> <!-- 与项目 Spring Boot 版本保持一致 -->
</dependency>
1.2 版本兼容性说明
| Spring Boot 版本 | Hibernate Validator 版本 | 支持的 JDK 版本 |
|---|---|---|
| 2.7.x | 6.2.x | JDK 8-17 |
| 3.0.x+ | 8.0.x+ | JDK 17+ |
注意:JDK 17 及以上版本需使用 Spring Boot 3.0+,否则可能出现注解解析异常。
2. 核心校验注解
Hibernate Validator 内置注解覆盖字符串、数字、集合、日期等主流数据类型的校验场景。
2.1 高频必用注解(覆盖率 ≥ 80%)
适用于日常开发中最常见的校验场景,建议优先掌握。
| 注解 | 适用类型 | 核心规则 | 项目实战示例 |
|---|---|---|---|
@NotBlank | String | 非 null 且 trim() 后长度 > 0(仅作用于字符串,如用户名、密码) | @NotBlank(message = "登录账号不能为空") |
@NotEmpty | String/Collection/Array | 非 null 且元素个数 > 0(字符串不 trim,集合需有元素) | @NotEmpty(message = "角色 ID 列表不能为空") |
@NotNull | 任意类型 | 不能为 null(不校验空值,如 Long 类型的 ID) | @NotNull(message = "用户 ID 不能为空") |
@Pattern(regexp) | String | 匹配指定正则表达式(如手机号、邮箱、密码复杂度) | @Pattern(regexp = "^[A-Za-z0-9_]{4,16}$", message = "账号仅支持字母、数字、下划线") |
@Length(min, max) | String | 字符串长度在 [min, max] 范围内(专用于字符串,比 @Size 更精准) | @Length(min = 6, max = 20, message = "密码长度为 6-20 位") |
@Range(min, max) | 数字/String | 数字值在 [min, max] 范围内;字符串长度在 [min, max] 范围内 | @Range(min = 1, max = 100, message = "年龄范围为 1-100 岁") |
@Email | String | 符合电子邮箱格式(支持自定义 regex 扩展,如限制企业邮箱) | @Email(regexp = "^[a-zA-Z0-9]+@company.com$", message = "仅支持企业邮箱") |
2.2 低频可选注解(特殊场景专用)
仅在特定业务场景下使用,按需选用。
| 注解 | 适用类型 | 核心规则 | 适用场景示例 |
|---|---|---|---|
@Null | 任意类型 | 必须为 null(与 @NotNull 相反,极少用) | 新增接口中 ID 需为 null(数据库自增) |
@DecimalMax(value) | 数字/String | 数值 ≤ 指定最大值(支持小数,如金额) | @DecimalMax(value = "99999.99", message = "单笔金额不超过 99999.99 元") |
@Past | LocalDateTime/Date | 时间必须在过去(如订单创建时间) | @Past(message = "订单创建时间必须为过去时间") |
@Future | LocalDateTime/Date | 时间必须在未来(如预约时间) | @Future(message = "预约时间必须为未来时间") |
@Positive | 数字类型 | 必须为正数(> 0,如数量、页数) | @Positive(message = "页码必须为正数") |
2.3 项目自定义注解(业务强相关)
内置注解无法满足业务需求时,通过自定义注解扩展。
| 注解 | 适用类型 | 核心功能 | 依赖工具/规则 |
|---|---|---|---|
@InDict | 任意类型 | 校验值是否在指定数据字典范围内(如状态:0-禁用/1-启用) | 依赖数据字典服务,需指定 dictType 属性:@InDict(dictType = "sys_user_status") |
@InEnum | 任意类型 | 校验值是否在指定枚举类的取值范围内(如性别:MALE/FEMALE) | 需指定枚举类:@InEnum(value = GenderEnum.class) |
@WeakPassword | String | 校验密码是否为弱密码(需包含大小写字母、数字、特殊符号) | 依赖 Pattern 正则:^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$ |
3. 实战落地:参数校验三步法
Spring Boot 项目中集成参数校验仅需「开启校验-添加规则-异常处理」三步,支持 Controller、Service 等多层级校验。
3.1 第一步:开启校验功能
在需要校验的类上添加 @Validated 注解,标识该类启用参数校验。注意:@Validated 是 Spring 提供的注解,而非 Hibernate Validator 原生注解。
| 组件类型 | 注解位置 | 示例代码 |
|---|---|---|
| Controller 层 | 类上(统一开启该类所有方法校验) | @RestController @RequestMapping("/demo") @Validated public class DemoController {} |
| Service 层 | 实现类上(避免接口耦合 Spring 注解) | @Service @Validated public class UserServiceImpl implements UserService {} |
| 工具类 | 类上(静态方法校验) | @Validated public class ValidateUtils {} |
关键疑问:Controller 校验后,Service 还需校验吗?
必须需要。原因如下:
Service 层可能被其他 Service 直接调用,跳过 Controller 层的校验;
分布式系统中,Service 可能被远程调用(如 Feign),参数合法性无法依赖上游校验;
业务逻辑变更可能导致 Controller校验规则与 Service 需求不一致,双重校验更可靠。
3.2 第二步:添加校验规则(按参数类型区分)
根据方法参数类型(Bean 类型/普通类型/嵌套类型),采用不同的规则添加方式。
场景 1:参数为 Bean 类型(最常用)
当参数是自定义 VO/DTO 时,需在方法参数上添加 @Valid 注解,并在 Bean 的属性上添加具体校验注解。
Step 1:定义 Bean 并添加属性校验注解
// 登录请求参数 Bean
@Data // Lombok 注解,省略 getter/setter
public class AuthLoginReqVO {
/** 登录账号 */
@NotBlank(message = "登录账号不能为空")
@Length(min = 4, max = 16, message = "账号长度为 4-16 位")
@Pattern(regexp = "^[A-Za-z0-9_]{4,16}$", message = "账号仅支持字母、数字、下划线")
private String username;
/** 手机号(可选,有值则校验格式) */
@Mobile(message = "手机号格式不正确") // 自定义注解,为空不校验
private String mobile;
}
Step 2:在 Controller/Service 方法中启用校验
// Controller 层示例
@PostMapping("/login")
public CommonResult<AuthLoginRespVO> login(
@RequestBody @Valid AuthLoginReqVO reqVO, // @Valid 触发 Bean 内校验
HttpServletRequest request) {
String ip = IpUtils.getIpAddr(request);
return authService.login(reqVO, ip);
}
// Service 层示例(接口定义)
public interface AuthService {
// @Valid 触发 Bean 内校验,确保 Service 被直接调用时也生效
CommonResult<AuthLoginRespVO> login(@Valid AuthLoginReqVO reqVO, String ip);
}
场景 2:参数为普通类型(基础类型/包装类型)
当参数是 Long、String 等普通类型时,直接在参数上添加校验注解。
// Controller 层示例:单个参数校验
@GetMapping("/user/get")
public CommonResult<UserRespVO> getUserById( @RequestParam("id")
@NotNull(message = "用户 ID 不能为空") Long id) {
return CommonResult.success(userService.getById(id));
}
// Service 层示例:多个参数校验
public interface UserService {
// 批量查询时校验 ID 列表
List<UserRespVO> getByIds(
@NotEmpty(message = "用户 ID 列表不能为空") List<@Range(min = 1) Long> ids);
}
场景 3:参数为嵌套 Bean 类型
当 Bean 中包含其他 Bean 类型字段时,需在嵌套字段上添加 @Valid 注解触发嵌套校验。
@Data
public class OrderCreateReqVO {
/** 订单基本信息 */
@Valid // 触发嵌套校验
private OrderBaseVO baseInfo;
/** 订单商品列表 */
@NotEmpty(message = "商品列表不能为空")
private List<@Valid OrderItemVO> itemList; // 集合内元素校验
// 嵌套 Bean
@Data
public static class OrderBaseVO {
@NotBlank(message = "订单编号不能为空")
private String orderNo;
@NotNull(message = "订单金额不能为空")
@Positive(message = "订单金额必须为正数")
private BigDecimal amount;
}
}
3.3 第三步:统一异常处理与响应
参数校验失败时,会抛出以下两种异常,需通过全局异常处理器统一捕获并返回标准响应。
异常类型说明
| 异常类 | 触发场景 |
|---|---|
ConstraintViolationException | 普通类型参数校验失败(如 @RequestParam 参数) |
MethodArgumentNotValidException | Bean 类型参数校验失败(如 @RequestBody 中的 Bean 属性) |
全局异常处理器实现
@RestControllerAdvice // 全局异常处理标识
@Slf4j
public class GlobalValidationExceptionHandler {
/**
* 处理普通类型参数校验失败异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public CommonResult<Void> handleConstraintViolationException(ConstraintViolationException e) {
// 提取第一个错误信息(避免返回过多冗余信息)
String errorMsg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElse("请求参数不正确");
log.warn("普通类型参数校验失败:{}", errorMsg);
return CommonResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg);
}
/**
* 处理 Bean 类型参数校验失败异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public CommonResult<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 提取字段关联的错误信息(格式:字段名:错误信息)
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
.collect(Collectors.joining(";"));
log.warn("Bean 类型参数校验失败:{}", errorMsg);
return CommonResult.fail(HttpStatus.BAD_REQUEST.value(), "请求参数不正确:" + errorMsg);
}
}
标准响应示例
- 普通类型参数校验失败:
{
"code": 400,
"data": null,
"msg": "请求参数不正确:用户 ID 不能为空"
}
- Bean 类型参数校验失败:
{
"code": 400,
"data": null,
"msg": "请求参数不正确:username:登录账号不能为空;password:密码需包含大小写字母、数字、特殊符号"
}
4. 进阶实践:解决复杂业务场景
4.1 分组校验(多场景复用 Bean)
同一 Bean 在不同场景(如新增/修改)的校验规则不同时,使用「分组校验」避免创建多个相似 Bean。
实现步骤
- 定义分组标识(空接口):
// 新增场景分组
public interface AddGroup {}
// 修改场景分组
public interface UpdateGroup {}
- Bean 中指定注解的分组:
@Data
public class UserSaveReqVO {
/** ID:新增时为 null,修改时必填 */
@NotNull(message = "用户 ID 不能为空", groups = UpdateGroup.class) // 仅修改场景校验
private Long id;
/** 用户名:新增/修改均必填 */
@NotBlank(message = "用户名不能为空", groups = {AddGroup.class, UpdateGroup.class})
private String username;
/** 密码:仅新增时必填 */
@NotBlank(message = "密码不能为空", groups = AddGroup.class) // 仅新增场景校验
private String password;
}
- 方法中指定校验分组:
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
// 新增用户:使用 AddGroup 分组规则
@PostMapping("/add")
public CommonResult<Void> add(@RequestBody @Validated(AddGroup.class) UserSaveReqVO reqVO) {
userService.add(reqVO);
return CommonResult.success();
}
// 修改用户:使用 UpdateGroup 分组规则
@PostMapping("/update")
public CommonResult<Void> update(@RequestBody @Validated(UpdateGroup.class) UserSaveReqVO reqVO) {
userService.update(reqVO);
return CommonResult.success();
}
}
4.2 手动触发校验(动态参数场景)
当参数是动态生成的(如前端动态表单),无法通过注解静态声明时,可通过 Validator 接口手动触发校验。
实现步骤
- 注入 Validator 实例:
@Service
public class OrderService {
// 注入 Hibernate Validator 实例
@Autowired
private Validator validator;
}
- 手动校验并处理结果:
public void createOrder(OrderCreateReqVO reqVO) {
// 手动触发校验
Set<ConstraintViolation<OrderCreateReqVO>> violations = validator.validate(reqVO);
// 处理校验失败结果
if (!violations.isEmpty()) {
String errorMsg = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
throw new BusinessException("订单创建失败:" + errorMsg);
}
// 业务逻辑...
}
4.3 自定义注解开发(以 @Mobile 为例)
当内置注解无法满足需求时,可通过「注解定义 + 校验器实现」两步自定义校验逻辑。
4.3.1 第一步:定义自定义注解
使用 @Constraint 注解指定校验器类,同时声明注解的基本属性(message、groups、payload):
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 支持字段和参数
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
@Constraint(validatedBy = MobileValidator.class) // 绑定校验器
public @interface Mobile {
// 校验失败提示信息(默认值)
String message() default "手机号格式不正确";
// 分组校验(默认无分组)
Class<?>[] groups() default {};
// 负载信息(用于传递额外数据,极少用)
Class<? extends Payload>[] payload() default {};
}
4.3.2 第二步:实现校验器
校验器需实现 ConstraintValidator<A, T> 接口,其中 A 是自定义注解类型,T 是待校验数据类型:
public class MobileValidator implements ConstraintValidator<Mobile, String> {
// 初始化方法(可获取注解属性,此处无需处理)
@Override
public void initialize(Mobile annotation) {}
// 核心校验逻辑
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 为空时不校验(如需必传,配合 @NotEmpty 使用)
if (StrUtil.isEmpty(value)) {
return true;
}
// 手机号正则(支持 11 位国内手机号)
String regex = "^1[3-9]\\d{9}$";
return Pattern.matches(regex, value);
}
}
4.3.3 第三步:使用自定义注解
在需要校验的参数或字段上直接添加自定义注解:
public class LoginReqVO {
@NotEmpty(message = "手机号不能为空")
@Mobile // 自定义手机号校验
private String mobile;
}
2646

被折叠的 条评论
为什么被折叠?



