Skip to content

参数校验最佳实践

介绍

RuoYi-Plus 框架深度集成了 Bean Validation(JSR-380)规范,提供了完善的参数校验机制。通过声明式的校验注解和分组校验功能,可以优雅地实现请求参数的自动验证,减少重复的校验代码,提升代码可维护性。

核心特性:

  • 声明式校验 - 使用注解声明校验规则,代码简洁直观
  • 分组校验 - 支持 AddGroup、EditGroup、QueryGroup 等校验分组,同一字段不同场景使用不同规则
  • 国际化支持 - 校验消息支持国际化,通过 I18nKeys 统一管理错误提示
  • 嵌套校验 - 支持嵌套对象的级联校验,使用 @Valid 注解触发
  • 自定义校验 - 支持自定义校验注解和校验器,满足特殊业务需求
  • XSS 防护 - 提供 @Xss 注解防止脚本注入攻击
  • 全局异常处理 - 校验失败自动返回友好的错误提示

基础校验注解

常用校验注解

Bean Validation 提供了丰富的内置校验注解:

注解说明适用类型
@NotNull值不能为 null所有类型
@NotBlank字符串不能为 null 且去除空格后长度大于 0String
@NotEmpty集合/数组/Map 不能为 null 且不能为空Collection/Array/Map
@Size限制长度或大小String/Collection/Array/Map
@Min数值最小值数值类型
@Max数值最大值数值类型
@DecimalMin精确数值最小值BigDecimal/String
@DecimalMax精确数值最大值BigDecimal/String
@Positive必须为正数数值类型
@PositiveOrZero必须为正数或零数值类型
@Negative必须为负数数值类型
@NegativeOrZero必须为负数或零数值类型
@Email邮箱格式String
@Pattern正则表达式匹配String
@Past必须是过去的时间Date/LocalDate/LocalDateTime
@Future必须是将来的时间Date/LocalDate/LocalDateTime
@Digits数字格式(整数位数和小数位数)数值类型
@Length字符串长度范围(Hibernate Validator)String
@Range数值范围(Hibernate Validator)数值类型
@URLURL 格式(Hibernate Validator)String

基本用法示例

java
package plus.ruoyi.system.config.domain.bo;

@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysConfig.class, reverseConvertGenerate = false)
public class SysConfigBo extends BaseEntity {

    /**
     * 参数主键
     */
    private Long configId;

    /**
     * 参数名称
     */
    @NotBlank(message = "参数名称不能为空")
    @Size(min = 0, max = 100, message = "参数名称不能超过{max}个字符")
    private String configName;

    /**
     * 参数键名
     */
    @NotBlank(message = "参数键名不能为空")
    @Size(min = 0, max = 100, message = "参数键名长度不能超过{max}个字符")
    private String configKey;

    /**
     * 参数键值
     */
    @NotBlank(message = "参数键值不能为空")
    @Size(min = 0, max = 500, message = "参数键值长度不能超过{max}个字符")
    private String configValue;

    /**
     * 系统内置(Y是 N否)
     */
    private String configType;

    /**
     * 备注
     */
    private String remark;
}

使用说明:

  • @NotBlank 用于字符串非空校验,会自动 trim
  • @Size 的 message 支持占位符 {max}{min}
  • 多个注解可以叠加使用,都会进行校验

数值校验示例

java
package plus.ruoyi.business.mall.domain.bo;

@Data
public class CreateOrderBo implements Serializable {

    /**
     * 商品ID
     */
    @NotNull(message = "商品ID不能为空")
    private Long goodsId;

    /**
     * 商品名称
     */
    @NotBlank(message = "商品名称不能为空")
    private String goodsName;

    /**
     * 商品价格
     */
    @NotNull(message = "商品价格不能为空")
    @DecimalMin(value = "0.01", message = "商品价格不能小于0.01元")
    private BigDecimal price;

    /**
     * 购买数量
     */
    @NotNull(message = "购买数量不能为空")
    @Min(value = 1, message = "购买数量不能小于1")
    private Long quantity;

    /**
     * 订单总金额(系统计算)
     */
    private BigDecimal totalAmount;
}

使用说明:

  • @NotNull 用于对象类型非空校验
  • @DecimalMin 用于 BigDecimal 类型的最小值校验
  • @Min 用于整数类型的最小值校验

用户信息校验示例

java
package plus.ruoyi.system.core.domain.bo;

@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysUser.class, reverseConvertGenerate = false)
public class SysUserBo extends BaseEntity {

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 部门ID
     */
    private Long deptId;

    /**
     * 用户账号
     */
    @Xss(message = "用户账号不能包含脚本字符")
    @NotBlank(message = "用户账号不能为空")
    @Size(min = 0, max = 30, message = "用户账号长度不能超过{max}个字符")
    private String userName;

    /**
     * 用户昵称
     */
    @Xss(message = "用户昵称不能包含脚本字符")
    @NotBlank(message = "用户昵称不能为空")
    @Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
    private String nickName;

    /**
     * 用户邮箱
     */
    @Email(message = "邮箱格式不正确")
    @Size(min = 0, max = 50, message = "邮箱长度不能超过{max}个字符")
    private String email;

    /**
     * 角色组
     */
    @Size(min = 1, message = "用户角色不能为空")
    private Long[] roleIds;
}

使用说明:

  • @Xss 是框架自定义注解,用于防止 XSS 攻击
  • @Email 用于邮箱格式校验
  • @Size 可用于数组长度校验

校验分组

分组接口定义

框架预定义了三个校验分组接口:

java
package plus.ruoyi.common.core.validate;

/**
 * 新增操作校验分组
 * 用于标识新增操作时需要进行的字段校验
 *
 * 常见使用场景:
 * - 新增时ID字段必须为空(使用@Null校验)
 * - 新增时必填字段不能为空(使用@NotNull、@NotBlank校验)
 * - 新增时需要进行唯一性校验的字段
 */
public interface AddGroup {
}
java
package plus.ruoyi.common.core.validate;

/**
 * 编辑操作校验分组
 * 用于标识编辑操作时需要进行的字段校验
 *
 * 常见使用场景:
 * - 编辑时ID字段不能为空(使用@NotNull校验)
 * - 编辑时可选填字段的格式校验
 * - 编辑时需要进行数据存在性校验的字段
 */
public interface EditGroup {
}
java
package plus.ruoyi.common.core.validate;

/**
 * 查询操作校验分组
 * 用于标识查询操作时需要进行的字段校验
 *
 * 常见使用场景:
 * - 分页查询时页码和每页条数的范围校验
 * - 查询时必要条件字段的非空校验
 */
public interface QueryGroup {
}

分组校验使用

在 Bo 对象中使用分组:

java
package plus.ruoyi.system.core.domain.bo;

@Data
public class SysRoleBo {

    /**
     * 角色ID
     * 新增时必须为空,编辑时必须不为空
     */
    @Null(groups = AddGroup.class, message = "新增时ID必须为空")
    @NotNull(groups = EditGroup.class, message = "编辑时ID不能为空")
    @Min(value = 1, groups = EditGroup.class, message = "ID必须大于0")
    private Long roleId;

    /**
     * 角色名称
     * 新增和编辑时都必填
     */
    @NotBlank(groups = {AddGroup.class, EditGroup.class}, message = "角色名称不能为空")
    @Size(max = 30, groups = {AddGroup.class, EditGroup.class}, message = "角色名称长度不能超过{max}个字符")
    private String roleName;

    /**
     * 角色权限字符串
     * 新增和编辑时都必填
     */
    @NotBlank(groups = {AddGroup.class, EditGroup.class}, message = "权限字符不能为空")
    @Size(max = 100, groups = {AddGroup.class, EditGroup.class}, message = "权限字符长度不能超过{max}个字符")
    private String roleKey;

    /**
     * 显示顺序
     * 新增时必填
     */
    @NotNull(groups = AddGroup.class, message = "显示顺序不能为空")
    private Integer roleSort;

    /**
     * 备注
     * 可选字段,仅校验长度
     */
    @Size(max = 500, message = "备注长度不能超过{max}个字符")
    private String remark;
}

在 Controller 中使用分组:

java
package plus.ruoyi.system.core.controller;

@Validated
@RestController
@RequestMapping("/system/role")
public class SysRoleController {

    private final ISysRoleService roleService;

    /**
     * 新增角色
     * 使用 AddGroup 分组校验
     */
    @PostMapping
    public R<Long> add(@Validated(AddGroup.class) @RequestBody SysRoleBo bo) {
        return R.ok(roleService.add(bo));
    }

    /**
     * 修改角色
     * 使用 EditGroup 分组校验
     */
    @PutMapping
    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysRoleBo bo) {
        return R.status(roleService.update(bo));
    }

    /**
     * 查询角色列表
     * 使用 QueryGroup 分组校验(如果需要)
     */
    @GetMapping("/list")
    public R<List<SysRoleVo>> list(@Validated(QueryGroup.class) SysRoleBo bo) {
        return R.ok(roleService.list(bo));
    }
}

代码生成器模板:

java
// 代码生成器生成的 Controller 模板
@Validated
@RestController
@RequestMapping("/${moduleName}/${businessName}")
public class ${ClassName}Controller {

    /**
     * 新增${functionName}
     */
    @PostMapping
    public R<${pkColumn.javaType}> add${BusinessName}(@Validated(AddGroup.class) @RequestBody ${ClassName}Bo bo) {
        return R.ok(${className}Service.add(bo));
    }

    /**
     * 修改${functionName}
     */
    @PutMapping
    public R<Void> update${BusinessName}(@Validated(EditGroup.class) @RequestBody ${ClassName}Bo bo) {
        return R.status(${className}Service.update(bo));
    }
}

分组校验规则

场景分组ID 字段必填字段可选字段
新增AddGroup@Null 必须为空@NotNull/@NotBlank仅格式校验
编辑EditGroup@NotNull 不能为空@NotNull/@NotBlank仅格式校验
查询QueryGroup可选根据业务需求仅格式校验

Controller 层校验

@Validated 与 @Valid 的区别

特性@Validated@Valid
来源Spring 框架JSR-380 标准
分组校验✅ 支持❌ 不支持
嵌套校验❌ 不支持✅ 支持
使用位置类、方法、参数方法、参数、字段

推荐使用方式:

  • Controller 类上使用 @Validated
  • 方法参数使用 @Validated(分组.class) 进行分组校验
  • 嵌套对象字段使用 @Valid 触发级联校验

基本校验

java
@Validated
@RestController
@RequestMapping("/api/order")
public class OrderPayController {

    private final IOrderService orderService;

    /**
     * 创建订单
     * 使用 @Validated 触发校验
     */
    @PostMapping("/create")
    public R<CreateOrderVo> createOrder(@Validated @RequestBody CreateOrderBo bo) {
        return R.ok(orderService.createOrder(bo));
    }

    /**
     * 取消订单
     * 直接在参数上使用校验注解
     */
    @PostMapping("/cancel")
    public R<Void> cancelOrder(
            @Validated @NotBlank(message = "订单号不能为空") @RequestParam String orderNo) {
        Long userId = LoginHelper.getUserId();
        return R.status(orderService.cancelOrder(orderNo, userId));
    }

    /**
     * 查询订单状态
     * 路径参数校验
     */
    @GetMapping("/status/{orderNo}")
    public R<Void> queryOrderStatus(
            @Validated @NotBlank(message = "订单号不为空") @PathVariable String orderNo) {
        // 查询逻辑
        return R.ok();
    }
}

嵌套对象校验

java
@Data
public class OrderBo {

    @NotNull(message = "订单信息不能为空")
    @Valid  // 触发嵌套对象校验
    private OrderInfoBo orderInfo;

    @NotEmpty(message = "商品列表不能为空")
    @Valid  // 触发集合中每个元素的校验
    private List<OrderItemBo> items;

    @Valid  // 可选嵌套对象,不为空时进行校验
    private ReceiverInfoBo receiverInfo;
}

@Data
public class OrderInfoBo {

    @NotBlank(message = "订单号不能为空")
    private String orderNo;

    @NotNull(message = "订单金额不能为空")
    @DecimalMin(value = "0.01", message = "订单金额必须大于0")
    private BigDecimal totalAmount;
}

@Data
public class OrderItemBo {

    @NotNull(message = "商品ID不能为空")
    private Long goodsId;

    @NotBlank(message = "商品名称不能为空")
    private String goodsName;

    @NotNull(message = "购买数量不能为空")
    @Min(value = 1, message = "购买数量至少为1")
    private Integer quantity;
}

集合参数校验

java
@Validated
@RestController
@RequestMapping("/api/batch")
public class BatchController {

    /**
     * 批量删除
     * 校验 ID 集合不能为空
     */
    @DeleteMapping
    public R<Void> batchDelete(
            @NotEmpty(message = "ID集合不能为空") @RequestBody List<Long> ids) {
        return R.status(service.batchDelete(ids));
    }

    /**
     * 批量新增
     * 校验集合不为空且每个元素都符合规则
     */
    @PostMapping("/add")
    public R<Void> batchAdd(
            @NotEmpty(message = "数据列表不能为空")
            @Valid @RequestBody List<ItemBo> items) {
        return R.status(service.batchAdd(items));
    }

    /**
     * 批量更新状态
     */
    @PutMapping("/status")
    public R<Void> batchUpdateStatus(
            @NotEmpty(message = "ID集合不能为空") @RequestParam List<Long> ids,
            @NotBlank(message = "状态不能为空") @RequestParam String status) {
        return R.status(service.batchUpdateStatus(ids, status));
    }
}

国际化消息

I18nKeys 常量定义

框架使用 I18nKeys 统一管理国际化消息 Key:

java
package plus.ruoyi.common.core.constant;

/**
 * 国际化消息 Key 常量
 */
public interface I18nKeys {

    interface Auth {
        String TYPE_REQUIRED = "auth.type.required";
        String CODE_REQUIRED = "auth.code.required";
        String CODE_EXPIRED = "auth.code.expired";
        String CODE_ERROR = "auth.code.error";
    }

    interface User {
        String USERNAME_REQUIRED = "user.username.required";
        String USERNAME_LENGTH_INVALID = "user.username.length.invalid";
        String PASSWORD_REQUIRED = "user.password.required";
        String PASSWORD_LENGTH_INVALID = "user.password.length.invalid";
        String EMAIL_FORMAT_INVALID = "user.email.format.invalid";
        String PHONE_FORMAT_INVALID = "user.phone.format.invalid";
    }

    interface Common {
        String ID_REQUIRED = "common.id.required";
        String NAME_REQUIRED = "common.name.required";
        String PARAM_ERROR = "common.param.error";
    }
}

使用国际化消息

java
package plus.ruoyi.common.core.domain.model;

@Data
public class PasswordLoginBody extends LoginBody {

    /**
     * 用户名
     */
    @NotBlank(message = I18nKeys.User.USERNAME_REQUIRED)
    @Length(min = 2, max = 30, message = I18nKeys.User.USERNAME_LENGTH_INVALID)
    private String userName;

    /**
     * 用户密码
     */
    @NotBlank(message = I18nKeys.User.PASSWORD_REQUIRED)
    @Length(min = 5, max = 30, message = I18nKeys.User.PASSWORD_LENGTH_INVALID)
    private String password;
}

消息资源文件

messages.properties 中定义消息:

properties
# 认证相关
auth.type.required=认证方式不能为空
auth.code.required=验证码不能为空
auth.code.expired=验证码已过期
auth.code.error=验证码错误

# 用户相关
user.username.required=用户名不能为空
user.username.length.invalid=用户名长度必须在{min}到{max}个字符之间
user.password.required=密码不能为空
user.password.length.invalid=密码长度必须在{min}到{max}个字符之间
user.email.format.invalid=邮箱格式不正确
user.phone.format.invalid=手机号格式不正确

# 通用
common.id.required=ID不能为空
common.name.required=名称不能为空
common.param.error=参数错误

英文消息文件 messages_en.properties:

properties
# Authentication
auth.type.required=Authentication type is required
auth.code.required=Verification code is required
auth.code.expired=Verification code has expired
auth.code.error=Verification code is incorrect

# User
user.username.required=Username is required
user.username.length.invalid=Username length must be between {min} and {max} characters
user.password.required=Password is required
user.password.length.invalid=Password length must be between {min} and {max} characters
user.email.format.invalid=Invalid email format
user.phone.format.invalid=Invalid phone number format

# Common
common.id.required=ID is required
common.name.required=Name is required
common.param.error=Invalid parameter

自定义校验

@Xss 防 XSS 攻击注解

框架提供了 @Xss 注解用于防止 XSS 脚本注入:

java
package plus.ruoyi.common.core.xss;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

/**
 * 自定义 XSS 校验注解
 * 防止脚本注入攻击
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Constraint(validatedBy = XssValidator.class)
public @interface Xss {

    String message() default "不允许包含脚本字符";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

校验器实现:

java
package plus.ruoyi.common.core.xss;

import cn.hutool.core.util.ReUtil;
import cn.hutool.http.HtmlUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * XSS 校验器
 */
public class XssValidator implements ConstraintValidator<Xss, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null || value.isEmpty()) {
            return true;
        }
        // 清除所有 HTML 标签后与原值比较
        return value.equals(HtmlUtil.cleanHtmlTag(value));
    }
}

使用示例:

java
@Data
public class SysUserBo {

    @Xss(message = "用户账号不能包含脚本字符")
    @NotBlank(message = "用户账号不能为空")
    @Size(max = 30, message = "用户账号长度不能超过{max}个字符")
    private String userName;

    @Xss(message = "用户昵称不能包含脚本字符")
    @NotBlank(message = "用户昵称不能为空")
    @Size(max = 30, message = "用户昵称长度不能超过{max}个字符")
    private String nickName;
}

自定义手机号校验注解

java
package plus.ruoyi.common.core.validate;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

/**
 * 手机号校验注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = PhoneValidator.class)
@Documented
public @interface Phone {

    String message() default "手机号格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 是否允许为空
     */
    boolean nullable() default false;
}

校验器实现:

java
package plus.ruoyi.common.core.validate;

import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * 手机号校验器
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {

    private boolean nullable;

    @Override
    public void initialize(Phone phone) {
        this.nullable = phone.nullable();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 如果允许为空且值为空,则通过
        if (nullable && StrUtil.isBlank(value)) {
            return true;
        }
        // 如果不允许为空但值为空,则不通过
        if (!nullable && StrUtil.isBlank(value)) {
            return false;
        }
        // 使用 Hutool 的手机号校验
        return Validator.isMobile(value);
    }
}

使用示例:

java
@Data
public class UserRegisterBo {

    @NotBlank(message = "用户名不能为空")
    private String userName;

    @Phone(message = "手机号格式不正确")
    private String phone;

    @Phone(nullable = true, message = "备用手机号格式不正确")
    private String backupPhone;
}

自定义身份证号校验注解

java
package plus.ruoyi.common.core.validate;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

/**
 * 身份证号校验注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = IdCardValidator.class)
@Documented
public @interface IdCard {

    String message() default "身份证号格式不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 是否允许为空
     */
    boolean nullable() default false;
}

校验器实现:

java
package plus.ruoyi.common.core.validate;

import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * 身份证号校验器
 */
public class IdCardValidator implements ConstraintValidator<IdCard, String> {

    private boolean nullable;

    @Override
    public void initialize(IdCard idCard) {
        this.nullable = idCard.nullable();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (nullable && StrUtil.isBlank(value)) {
            return true;
        }
        if (!nullable && StrUtil.isBlank(value)) {
            return false;
        }
        // 使用 Hutool 的身份证校验
        return Validator.isCitizenId(value);
    }
}

自定义枚举值校验注解

java
package plus.ruoyi.common.core.validate;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

/**
 * 枚举值校验注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Constraint(validatedBy = EnumValueValidator.class)
@Documented
public @interface EnumValue {

    String message() default "枚举值不正确";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * 允许的枚举值
     */
    String[] values();

    /**
     * 是否允许为空
     */
    boolean nullable() default false;
}

校验器实现:

java
package plus.ruoyi.common.core.validate;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

/**
 * 枚举值校验器
 */
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {

    private String[] values;
    private boolean nullable;

    @Override
    public void initialize(EnumValue enumValue) {
        this.values = enumValue.values();
        this.nullable = enumValue.nullable();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (nullable && StrUtil.isBlank(value)) {
            return true;
        }
        if (!nullable && StrUtil.isBlank(value)) {
            return false;
        }
        return ArrayUtil.contains(values, value);
    }
}

使用示例:

java
@Data
public class OrderBo {

    @NotBlank(message = "订单状态不能为空")
    @EnumValue(values = {"pending", "paid", "shipped", "completed", "cancelled"},
               message = "订单状态不正确,允许值: pending, paid, shipped, completed, cancelled")
    private String orderStatus;

    @EnumValue(values = {"wechat", "alipay"}, nullable = true,
               message = "支付方式不正确")
    private String paymentMethod;
}

异常处理集成

全局校验异常处理

框架在 GlobalExceptionHandler 中统一处理校验异常:

java
package plus.ruoyi.common.web.handler;

@Slf4j
@RestControllerAdvice
@Order(Integer.MAX_VALUE)
public class GlobalExceptionHandler {

    /**
     * 处理数据绑定异常
     * 场景: 表单数据绑定到对象时发生的验证失败
     */
    @ExceptionHandler(BindException.class)
    public R<Void> handleBindException(BindException e) {
        log.error(e.getMessage());
        String message = StreamUtils.join(e.getAllErrors(),
                DefaultMessageSourceResolvable::getDefaultMessage, ", ");
        return R.fail(message);
    }

    /**
     * 处理约束违反异常
     * 场景: Bean Validation 注解验证失败
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public R<Void> constraintViolationException(ConstraintViolationException e) {
        log.error(e.getMessage());
        String message = StreamUtils.join(e.getConstraintViolations(),
                ConstraintViolation::getMessage, ", ");
        return R.fail(message);
    }

    /**
     * 处理方法参数验证异常
     * 场景: @RequestBody + @Valid 注解的 JSON 对象验证失败
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error(e.getMessage());
        String message = StreamUtils.join(e.getBindingResult().getAllErrors(),
                DefaultMessageSourceResolvable::getDefaultMessage, ", ");
        return R.fail(message);
    }
}

异常响应示例

校验失败时返回的响应格式:

json
{
  "code": 500,
  "msg": "商品ID不能为空, 商品名称不能为空, 商品价格不能为空",
  "data": null
}

单个字段校验失败:

json
{
  "code": 500,
  "msg": "手机号格式不正确",
  "data": null
}

最佳实践

1. 合理使用校验分组

✅ 推荐:

java
@Data
public class ArticleBo {

    /**
     * 文章ID
     * 新增时必须为空,编辑时必须存在
     */
    @Null(groups = AddGroup.class, message = "新增时ID必须为空")
    @NotNull(groups = EditGroup.class, message = "编辑时ID不能为空")
    private Long id;

    /**
     * 文章标题
     * 新增和编辑时都必填
     */
    @NotBlank(groups = {AddGroup.class, EditGroup.class}, message = "文章标题不能为空")
    @Size(max = 200, message = "文章标题不能超过{max}个字符")
    private String title;

    /**
     * 文章内容
     * 新增时必填,编辑时可选
     */
    @NotBlank(groups = AddGroup.class, message = "文章内容不能为空")
    private String content;

    /**
     * 发布状态
     * 仅在编辑时校验
     */
    @EnumValue(values = {"draft", "published", "archived"},
               groups = EditGroup.class, message = "发布状态不正确")
    private String status;
}

❌ 不推荐:

java
@Data
public class ArticleBo {

    // ❌ 没有使用分组,无法区分新增和编辑场景
    @NotNull(message = "ID不能为空")
    private Long id;

    @NotBlank(message = "文章标题不能为空")
    private String title;
}

2. 校验消息清晰明确

✅ 推荐:

java
@Data
public class UserBo {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在{min}到{max}个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    private String userName;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在{min}到{max}个字符之间")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
             message = "密码必须包含大小写字母和数字")
    private String password;

    @Email(message = "邮箱格式不正确,请输入有效的邮箱地址")
    private String email;
}

❌ 不推荐:

java
@Data
public class UserBo {

    // ❌ 消息不够明确
    @NotBlank(message = "错误")
    @Size(min = 2, max = 20, message = "长度错误")
    private String userName;

    // ❌ 使用默认消息,用户看不懂
    @NotBlank
    @Size(min = 6, max = 20)
    private String password;
}

3. 前后端校验配合

✅ 推荐:

前端进行基础校验,提升用户体验;后端进行完整校验,确保数据安全。

typescript
// 前端校验 - 即时反馈
const rules = {
  userName: [
    { required: true, message: '用户名不能为空' },
    { min: 2, max: 20, message: '用户名长度必须在2到20个字符之间' },
    { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
  ],
  password: [
    { required: true, message: '密码不能为空' },
    { min: 6, max: 20, message: '密码长度必须在6到20个字符之间' }
  ],
  email: [
    { type: 'email', message: '邮箱格式不正确' }
  ]
}
java
// 后端校验 - 安全保障
@Data
public class UserBo {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在{min}到{max}个字符之间")
    @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
    @Xss(message = "用户名不能包含脚本字符")
    private String userName;

    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 20, message = "密码长度必须在{min}到{max}个字符之间")
    private String password;

    @Email(message = "邮箱格式不正确")
    private String email;
}

4. 避免过度校验

✅ 推荐:

java
@Data
public class QueryBo {

    // 查询参数通常不需要严格的非空校验
    private String keyword;

    // 分页参数有默认值,不需要 @NotNull
    private Integer pageNum = 1;
    private Integer pageSize = 10;

    // 日期范围可选
    private LocalDateTime startTime;
    private LocalDateTime endTime;
}

❌ 不推荐:

java
@Data
public class QueryBo {

    // ❌ 搜索关键词不应该必填
    @NotBlank(message = "关键词不能为空")
    private String keyword;

    // ❌ 分页参数有默认值,不需要必填校验
    @NotNull(message = "页码不能为空")
    private Integer pageNum;

    @NotNull(message = "每页条数不能为空")
    private Integer pageSize;
}

5. 使用自定义校验封装复杂规则

✅ 推荐:

java
// 使用自定义注解封装复杂校验逻辑
@Data
public class OrderBo {

    @Phone(message = "收货人手机号格式不正确")
    private String receiverPhone;

    @IdCard(nullable = true, message = "身份证号格式不正确")
    private String idCard;

    @EnumValue(values = {"express", "ems", "sf"}, message = "快递类型不正确")
    private String expressType;
}

❌ 不推荐:

java
// ❌ 在每个字段上重复写正则表达式
@Data
public class OrderBo {

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "收货人手机号格式不正确")
    private String receiverPhone;

    @Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$",
             message = "身份证号格式不正确")
    private String idCard;
}

6. Service 层补充业务校验

✅ 推荐:

java
@Service
public class OrderServiceImpl implements IOrderService {

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(CreateOrderBo bo) {
        // 1. Bean Validation 已完成基础校验
        // 2. Service 层进行业务规则校验

        // 检查商品是否存在
        Product product = productDao.getById(bo.getGoodsId());
        ServiceException.notNull(product, "商品不存在");

        // 检查商品是否上架
        ServiceException.throwIf(!product.getOnline(), "商品已下架");

        // 检查库存是否充足
        ServiceException.throwIf(product.getStock() < bo.getQuantity(),
                "库存不足,剩余库存: {}", product.getStock());

        // 检查价格是否一致(防止前端篡改)
        ServiceException.throwIf(!product.getPrice().equals(bo.getPrice()),
                "商品价格已变化,请刷新页面");

        // 创建订单
        Order order = buildOrder(bo, product);
        orderDao.insert(order);

        return order.getId();
    }
}

校验分工:

校验类型位置说明
格式校验Bo + Bean Validation非空、长度、格式等
业务校验Service数据存在性、状态、权限等
数据校验DAO/数据库唯一约束、外键约束等

常见问题

1. @Validated 不生效

问题原因:

  • Controller 类上没有添加 @Validated 注解
  • 方法参数上没有添加 @Validated@Valid
  • 校验注解没有指定分组但使用了分组校验

✅ 正确做法:

java
@Validated  // 类上必须添加
@RestController
@RequestMapping("/api/user")
public class UserController {

    @PostMapping
    public R<Long> add(@Validated(AddGroup.class) @RequestBody UserBo bo) {
        // 方法参数上必须添加 @Validated
        return R.ok(userService.add(bo));
    }
}
java
@Data
public class UserBo {

    // 校验注解必须指定分组
    @NotBlank(groups = AddGroup.class, message = "用户名不能为空")
    private String userName;
}

2. 嵌套对象校验不生效

问题原因:

  • 嵌套对象字段上没有添加 @Valid 注解

❌ 错误示例:

java
@Data
public class OrderBo {

    // ❌ 缺少 @Valid,嵌套对象不会被校验
    @NotNull(message = "收货信息不能为空")
    private ReceiverInfoBo receiverInfo;
}

✅ 正确做法:

java
@Data
public class OrderBo {

    // ✅ 添加 @Valid 触发嵌套校验
    @NotNull(message = "收货信息不能为空")
    @Valid
    private ReceiverInfoBo receiverInfo;
}

3. 集合元素校验不生效

问题原因:

  • 集合字段上没有添加 @Valid 注解

❌ 错误示例:

java
@Data
public class BatchBo {

    // ❌ 只校验集合不为空,不校验元素
    @NotEmpty(message = "数据列表不能为空")
    private List<ItemBo> items;
}

✅ 正确做法:

java
@Data
public class BatchBo {

    // ✅ 添加 @Valid 校验集合中的每个元素
    @NotEmpty(message = "数据列表不能为空")
    @Valid
    private List<ItemBo> items;
}

4. 分组校验规则冲突

问题原因:

  • 同一字段在不同分组中的校验规则相互矛盾

❌ 错误示例:

java
@Data
public class UserBo {

    // ❌ 新增时必须为空,但 @NotNull 没有指定分组
    @NotNull(message = "ID不能为空")  // 所有场景都生效
    @Null(groups = AddGroup.class, message = "新增时ID必须为空")
    private Long id;
}

✅ 正确做法:

java
@Data
public class UserBo {

    // ✅ 明确指定每个注解的分组
    @Null(groups = AddGroup.class, message = "新增时ID必须为空")
    @NotNull(groups = EditGroup.class, message = "编辑时ID不能为空")
    private Long id;
}

5. 校验消息占位符不生效

问题原因:

  • 占位符格式错误或注解不支持

❌ 错误示例:

java
@Data
public class UserBo {

    // ❌ @NotBlank 不支持 {min} 和 {max} 占位符
    @NotBlank(message = "用户名长度必须在{min}到{max}之间")
    private String userName;
}

✅ 正确做法:

java
@Data
public class UserBo {

    @NotBlank(message = "用户名不能为空")
    @Size(min = 2, max = 20, message = "用户名长度必须在{min}到{max}个字符之间")
    private String userName;
}

支持的占位符:

注解支持的占位符
@Size{min}, {max}
@Length{min}, {max}
@Min{value}
@Max{value}
@DecimalMin{value}
@DecimalMax{value}
@Range{min}, {max}
@Digits{integer}, {fraction}

总结

RuoYi-Plus 框架的参数校验体系提供了完善的验证机制:

核心组件:

  • Bean Validation 注解 - 声明式校验规则
  • 校验分组(AddGroup/EditGroup/QueryGroup) - 同一字段不同场景的校验
  • 自定义校验注解 - 封装复杂校验逻辑
  • 全局异常处理 - 统一返回友好错误信息

最佳实践:

  • 合理使用校验分组,区分新增/编辑/查询场景
  • 校验消息清晰明确,使用占位符动态显示参数
  • 前后端配合校验,前端提升体验,后端保证安全
  • 避免过度校验,查询参数通常不需要严格的非空校验
  • 使用自定义注解封装复杂校验逻辑
  • Service 层补充业务规则校验

校验分工:

  • Controller 层 - 基础格式校验(Bean Validation)
  • Service 层 - 业务规则校验(ServiceException)
  • DAO 层 - 数据约束校验(数据库约束)

通过遵循这些最佳实践,可以构建健壮、可维护的参数校验体系,提升系统安全性和用户体验。