后端_API 参数校验

前言

在 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.x6.2.xJDK 8-17
3.0.x+8.0.x+JDK 17+

注意:JDK 17 及以上版本需使用 Spring Boot 3.0+,否则可能出现注解解析异常。

2. 核心校验注解

Hibernate Validator 内置注解覆盖字符串、数字、集合、日期等主流数据类型的校验场景。

2.1 高频必用注解(覆盖率 ≥ 80%)

适用于日常开发中最常见的校验场景,建议优先掌握

注解适用类型核心规则项目实战示例
@NotBlankString非 null 且 trim() 后长度 > 0(仅作用于字符串,如用户名、密码)@NotBlank(message = "登录账号不能为空")
@NotEmptyString/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 岁")
@EmailString符合电子邮箱格式(支持自定义 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 元")
@PastLocalDateTime/Date时间必须在过去(如订单创建时间)@Past(message = "订单创建时间必须为过去时间")
@FutureLocalDateTime/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)
@WeakPasswordString校验密码是否为弱密码(需包含大小写字母、数字、特殊符号)依赖 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:参数为普通类型(基础类型/包装类型)

当参数是 LongString 等普通类型时,直接在参数上添加校验注解。

// 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 参数)
MethodArgumentNotValidExceptionBean 类型参数校验失败(如 @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);
    }
}
标准响应示例
  1. 普通类型参数校验失败:
{
  "code": 400,
  "data": null,
  "msg": "请求参数不正确:用户 ID 不能为空"
}
  1. Bean 类型参数校验失败:
{
  "code": 400,
  "data": null,
  "msg": "请求参数不正确:username:登录账号不能为空;password:密码需包含大小写字母、数字、特殊符号"
}

4. 进阶实践:解决复杂业务场景

4.1 分组校验(多场景复用 Bean)

同一 Bean 在不同场景(如新增/修改)的校验规则不同时,使用「分组校验」避免创建多个相似 Bean。

实现步骤
  1. 定义分组标识(空接口)
// 新增场景分组
public interface AddGroup {}

// 修改场景分组
public interface UpdateGroup {}
  1. 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;
}
  1. 方法中指定校验分组
@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 接口手动触发校验。

实现步骤
  1. 注入 Validator 实例
@Service
public class OrderService {

    // 注入 Hibernate Validator 实例
    @Autowired
    private Validator validator;
}
  1. 手动校验并处理结果
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;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值