JSR303参数校验技术方案详解

一、为什么需要参数校验

1.1 常见认知误区

误区:很多后端开发人员认为参数校验是前端的事情,后端不用管。

这种想法是错误的!原因如下:

分析角度 前端校验 后端校验
目的 提升用户体验 保障数据安全
可绕过性 ❌ 可被绕过(禁用JS、Postman直调接口) ✅ 无法绕过
防护范围 正常用户的输入错误 恶意攻击、异常请求
数据完整性 ❌ 无法保证 ✅ 最后一道防线

正确做法:前后端双重校验,各司其职

用户输入 → 前端校验(提升体验) → 后端校验(保障安全) → 数据入库
                    ↓                    ↓
              友好提示             拦截恶意请求

典型案例

  • 前端禁用JS后,可直接调用后端接口
  • 使用Postman等工具可绕过前端直接请求
  • 前端校验可被修改,后端校验不可篡改

1.2 不做参数校验的风险

风险类型 具体表现 后果
安全风险 SQL注入、XSS攻击、恶意数据注入 数据泄露、系统被攻击
数据风险 脏数据入库、必填字段为空、数据格式错误 业务逻辑异常、数据一致性破坏
系统风险 NullPointerException、NumberFormatException、类型转换异常 服务崩溃、500错误
维护风险 异常场景难以复现、错误定位困难 排查成本高、修复周期长
体验风险 错误提示模糊、边界场景未处理 用户困惑、投诉增加

1.3 典型问题案例

// ❌ 不做校验的危险代码
@PostMapping("/user")
public Result addUser(@RequestBody UserDTO user) {
    // 若user为null,直接空指针
    // 若name为null,入库失败
    // 若age为负数,业务异常
    return Result.success(userService.save(user));
}

// ✅ 手动校验(代码冗余)
@PostMapping("/user")
public Result addUser(@RequestBody UserDTO user) {
    if (user == null) {
        return Result.error("参数不能为空");
    }
    if (StringUtils.isBlank(user.getName())) {
        return Result.error("姓名不能为空");
    }
    if (user.getAge() == null || user.getAge() < 0 || user.getAge() > 150) {
        return Result.error("年龄不合法");
    }
    // ... 更多校验
    return Result.success(userService.save(user));
}

二、解决方案对比

方案 优点 缺点 适用场景
手动if-else 简单直观、完全可控 代码冗余、维护困难、不可复用 简单校验、一次性逻辑
JSR303注解 声明式、简洁、可复用、支持分组 需学习注解规范 推荐方案
Validator工具 灵活、可定制 代码量大、需封装 复杂业务规则

✅ 推荐使用JSR303注解校验,配合全局异常处理


三、JSR303规范介绍

3.1 规范与实现的关系

重要:JSR303只是一套规范/标准,定义了参数校验的注解和API接口,本身不提供具体实现。

规范演进

规范 版本 JDK 说明
JSR303 Bean Validation 1.0 JDK6+ 基础校验注解
JSR349 Bean Validation 1.1 JDK6+ 增加方法校验
JSR380 Bean Validation 2.0 JDK8+ 支持容器校验(List、Optional)

实现框架

实现框架 说明
Hibernate Validator 官方参考实现,Spring Boot默认集成,功能最完善
Apache BVal Apache提供的实现,功能相对较少

Spring Boot中的依赖关系

spring-boot-starter-validation
    └── hibernate-validator  (具体实现)
            └── jakarta.validation-api  (JSR303规范API)

3.2 启用参数校验的步骤

Step 1:引入依赖

<!-- Spring Boot 2.3+ 需手动引入 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- 或直接引入Hibernate Validator -->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.5.Final</version>
</dependency>

注意:Spring Boot 2.0-2.2.x版本默认包含该依赖,2.3.x+需要手动添加。

Step 2:配置全局异常处理器(可选但推荐)

用于统一处理校验异常,返回友好错误提示。

Step 3:在DTO和Controller中使用校验注解

无需额外配置,Spring自动识别并执行校验。


四、如何使用JSR303

4.1 @Valid 与 @Validated 的区别

对比项 @Valid @Validated
来源 JSR303标准注解 Spring扩展注解
分组校验 ❌ 不支持 ✅ 支持指定校验组
嵌套校验 ✅ 支持级联校验 ✅ 支持级联校验
使用位置 方法参数、字段、构造器 类、方法参数
异常类型 MethodArgumentNotValidException BindException(表单)/ ConstraintViolationException(方法参数)

选用建议

场景 推荐使用 示例
基础校验(无分组) @Valid@Validated @Valid @RequestBody UserDTO user
分组校验 @Validated(XXXGroup.class) @Validated(AddGroup.class) @RequestBody UserDTO user
嵌套对象级联校验 字段上用 @Valid @Valid private AddressDTO address;
Service层方法校验 类上用 @Validated @Validated public class UserService {}
单参数校验(@RequestParam/@PathVariable) 类上用 @Validated Controller类级别添加

4.2 常用注解速查表

注解 适用类型 说明 示例
@NotNull 任意类型 不能为null @NotNull(message = "ID不能为空")
@NotBlank String 不能为null且trim后长度>0 @NotBlank(message = "姓名不能为空")
@NotEmpty String/Collection/Map 不能为null且size/length>0 @NotEmpty(message = "列表不能为空")
@Size String/Collection 长度/元素数量范围 @Size(min=1, max=10)
@Length String 字符串长度范围(Hibernate扩展) @Length(min=2, max=50)
@Min / @Max 数字类型 数值范围 @Min(value = 0, message = "不能为负")
@Pattern String 正则匹配 @Pattern(regexp = "^1[3-9]\\d{9}$")
@Email String 邮箱格式 @Email(message = "邮箱格式错误")
@Digits 数字类型 整数位和小数位限制 @Digits(integer=5, fraction=2)
@Past / @Future 日期类型 过去/未来日期 @Past(message = "必须是过去日期")
@DecimalMin / @DecimalMax BigDecimal 小数范围 @DecimalMin("0.01")
@Positive / @PositiveOrZero 数字类型 正数/非负数(JSR380新增) @Positive
@Negative / @NegativeOrZero 数字类型 负数/非正数(JSR380新增) @Negative

4.3 基本使用步骤

Step 1:在DTO上添加注解

public class UserDTO {
    
    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 20, message = "用户名长度2-20位")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", 
             message = "密码必须包含大小写字母和数字,至少8位")
    private String password;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 1, message = "年龄必须大于0")
    @Max(value = 150, message = "年龄不能超过150")
    private Integer age;
    
    @Email(message = "邮箱格式不正确")
    private String email;
    
    // getter/setter...
}

Step 2:Controller启用校验

@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @PostMapping
    public Result addUser(@Valid @RequestBody UserDTO user) {
        return Result.success(userService.save(user));
    }
}

Step 3:全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理 @RequestBody 参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        String message = result.getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }
    
    /**
     * 处理表单/QueryString参数校验异常
     */
    @ExceptionHandler(BindException.class)
    public Result handleBindException(BindException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }
    
    /**
     * 处理 @RequestParam @PathVariable 校验异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Result handleConstraintViolation(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }
}

五、使用规范与要求

5.1 校验分组

问题:新增时ID为空,更新时ID必填,如何复用同一个DTO?

解决方案:使用分组校验

// 分组接口定义
public interface AddGroup {}   // 新增分组
public interface UpdateGroup {} // 更新分组

// DTO中使用groups指定校验组
public class UserDTO {
    
    @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;  // 仅新增时必填
}

// Controller中指定校验组
@PostMapping
public Result add(@Validated(AddGroup.class) @RequestBody UserDTO user) {
    return Result.success(userService.save(user));
}

@PutMapping
public Result update(@Validated(UpdateGroup.class) @RequestBody UserDTO user) {
    return Result.success(userService.update(user));
}

⚠️ 注意:未指定 groups 的注解,在使用 @Validated(XXXGroup.class)不会生效

5.2 嵌套校验

对象内包含对象时,使用 @Valid 触发级联校验:

public class OrderDTO {
    
    @NotBlank(message = "订单号不能为空")
    private String orderNo;
    
    @NotNull(message = "收货地址不能为空")
    @Valid  // 关键:触发嵌套校验
    private AddressDTO address;
}

public class AddressDTO {
    
    @NotBlank(message = "省份不能为空")
    private String province;
    
    @NotBlank(message = "城市不能为空")
    private String city;
    
    @NotBlank(message = "详细地址不能为空")
    private String detail;
}

5.3 集合校验

public class BatchDTO {
    
    @NotEmpty(message = "用户列表不能为空")
    @Size(max = 100, message = "单次最多处理100条")
    @Valid  // 校验集合内每个元素
    private List<@Valid UserDTO> users;
}

5.4 自定义校验注解

场景:校验手机号、身份证、枚举值等业务特定规则

5.4.1 手机号校验示例

Step 1:定义注解

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
    String message() default "手机号格式不正确";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2:实现校验器

public class PhoneValidator implements ConstraintValidator<Phone, String> {
    
    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
    
    @Override
    public void initialize(Phone constraintAnnotation) {
        // 初始化配置(可选)
    }
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;  // null不校验,由@NotBlank处理
        }
        return PHONE_PATTERN.matcher(value).matches();
    }
}

Step 3:使用自定义注解

public class UserDTO {
    @NotBlank(message = "手机号不能为空")
    @Phone(message = "手机号格式不正确")
    private String phone;
}

5.4.2 枚举值校验示例

Step 1:定义注解

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
@Documented
public @interface EnumValue {
    String message() default "值不在允许的枚举范围内";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    
    /**
     * 目标枚举类
     */
    Class<? extends Enum<?>> enumClass();

    /**
     * 枚举中获取值的方法名(如 getCode、getValue)
     * 默认使用 name() 方法返回枚举常量名
     */
    String enumMethod() default "name";
}

Step 2:实现校验器

/**
 * 枚举值校验器 - 推荐使用版本
 *
 * 特点:
 * 1. 不支持不稳定的 ordinal 校验
 * 2. 强制要求指定具体的枚举属性方法(如 getCode、getValue)
 * 3. 支持缓存反射方法,提高性能
 * 4. 提供更清晰的错误信息
 */
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

    private Class<? extends Enum<?>> enumClass;
    private String enumMethod;
    private Map<Object, Enum<?>> enumValueMap;
    private String cacheKey;

    @Override
    public void initialize(EnumValue constraintAnnotation) {
        this.enumClass = constraintAnnotation.enumClass();
        this.enumMethod = constraintAnnotation.enumMethod();

        // 生成缓存键,避免重复反射
        this.cacheKey = enumClass.getName() + "#" + enumMethod;

        // 预加载并缓存枚举值映射
        loadEnumValues();
    }

    /**
     * 加载枚举值并建立映射
     */
    private void loadEnumValues() {
        try {
            Enum<?>[] enumConstants = enumClass.getEnumConstants();
            Method method = enumClass.getMethod(enumMethod);

            // 创建值到枚举的映射
            enumValueMap = Arrays.stream(enumConstants)
                    .collect(Collectors.toMap(
                            enumConstant -> {
                                try {
                                    return method.invoke(enumConstant);
                                } catch (Exception e) {
                                    throw new RuntimeException("无法调用枚举方法: " + enumMethod, e);
                                }
                            },
                            enumConstant -> enumConstant
                    ));
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException(String.format(
                    "枚举 %s 中不存在方法 %s(),请检查方法名是否正确",
                    enumClass.getSimpleName(), enumMethod
            ), e);
        }
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 空值校验交给 @NotNull 处理
        if (value == null) {
            return true;
        }

        // 如果传入的就是枚举类型,则直接检查是否属于该枚举,是则直接返回
        if (enumClass.isInstance(value)) {
            return true;
        }

        // 按检查值判断是否存在于枚举映射中
        boolean isValid = enumValueMap.containsKey(value);

        if (!isValid) {
            // 构建友好的错误信息
            buildValidationError(context, value);
        }

        return isValid;
    }

    /**
     * 构建验证错误信息
     */
    private void buildValidationError(ConstraintValidatorContext context, Object value) {
        context.disableDefaultConstraintViolation();

        String allowedValues = enumValueMap.keySet().stream()
                .map(Object::toString)
                .collect(Collectors.joining(", "));

        String message = String.format(
                "无效的值 '%s'。允许的值: [%s]",
                value, allowedValues
        );

        context.buildConstraintViolationWithTemplate(message)
                .addConstraintViolation();
    }
}

Step 3:定义枚举类

public enum UserStatusEnum {
    ENABLED(1, "启用"),
    DISABLED(0, "禁用"),
    DELETED(-1, "已删除");

    private final int code;
    private final String desc;

    UserStatus(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

Step 4:使用自定义注解

public class UserDTO {

    @NotNull(message = "状态不能为空")
    @EnumValue(
            enumClass = UserStatusEnum.class,
            enumMethod = "getCode",
            message = "状态值无效"
    )
    private Integer status;

    // getter/setter
}

5.5 Service层手动校验

@Service
@Validated  // 启用方法级别校验
public class UserService {
    
    public void updateUser(@Valid UserDTO user) {
        // 方法入参自动校验
    }
    
    // 手动触发校验
    @Autowired
    private Validator validator;
    
    public void batchImport(List<UserDTO> users) {
        for (UserDTO user : users) {
            Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
            if (!violations.isEmpty()) {
                throw new BusinessException(
                    violations.iterator().next().getMessage()
                );
            }
        }
    }
}

六、使用场景示例

6.1 用户登录参数校验

public class LoginDTO {
    
    @NotBlank(message = "用户名不能为空")
    @Length(min = 2, max = 50, message = "用户名长度2-50位")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Length(min = 6, max = 20, message = "密码长度6-20位")
    private String password;
}

6.2 分页查询参数校验

public class PageQueryDTO {
    
    @Min(value = 1, message = "页码最小为1")
    private Integer pageNum = 1;
    
    @Min(value = 1, message = "每页条数最小为1")
    @Max(value = 100, message = "每页条数最大为100")
    private Integer pageSize = 10;
    
    @Length(max = 100, message = "搜索关键词最长100字符")
    private String keyword;
}

6.3 条件组合校验(类级别校验)

场景:当type=1时,amount必填;type=2时,rate必填

Step 1:定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = InvestmentTypeValidator.class)
public @interface ValidInvestment {
    String message() default "投资参数不完整";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2:实现校验器

public class InvestmentTypeValidator implements ConstraintValidator<ValidInvestment, InvestmentDTO> {
    
    @Override
    public boolean isValid(InvestmentDTO dto, ConstraintValidatorContext context) {
        if (dto == null) {
            return true;
        }
        
        // 禁用默认错误消息
        context.disableDefaultConstraintViolation();
        
        Integer type = dto.getType();
        
        if (type == null) {
            context.buildConstraintViolationWithTemplate("投资类型不能为空")
                   .addPropertyNode("type")
                   .addConstraintViolation();
            return false;
        }
        
        // type=1时,amount必填
        if (type == 1 && dto.getAmount() == null) {
            context.buildConstraintViolationWithTemplate("固定金额投资时,金额不能为空")
                   .addPropertyNode("amount")
                   .addConstraintViolation();
            return false;
        }
        
        // type=2时,rate必填
        if (type == 2 && dto.getRate() == null) {
            context.buildConstraintViolationWithTemplate("比例投资时,比例不能为空")
                   .addPropertyNode("rate")
                   .addConstraintViolation();
            return false;
        }
        
        return true;
    }
}

Step 3:使用注解

@ValidInvestment
public class InvestmentDTO {
    private Integer type;      // 1-固定金额 2-比例
    private BigDecimal amount; // 固定金额
    private BigDecimal rate;   // 比例
}

6.4 日期范围校验(类级别校验)

场景:结束日期必须大于开始日期

Step 1:定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
    String message() default "结束日期必须大于开始日期";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Step 2:实现校验器

public class DateRangeValidator implements ConstraintValidator<ValidDateRange, DateRangeDTO> {
    
    @Override
    public boolean isValid(DateRangeDTO dto, ConstraintValidatorContext context) {
        if (dto == null || dto.getStartDate() == null || dto.getEndDate() == null) {
            return true; // null值由其他注解处理
        }
        
        if (dto.getEndDate().isBefore(dto.getStartDate())) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("结束日期必须大于开始日期")
                   .addPropertyNode("endDate")
                   .addConstraintViolation();
            return false;
        }
        
        return true;
    }
}

Step 3:使用注解

@ValidDateRange
public class DateRangeDTO {
    
    @NotNull(message = "开始日期不能为空")
    private LocalDate startDate;
    
    @NotNull(message = "结束日期不能为空")
    private LocalDate endDate;
}

6.5 @PathVariable 和 @RequestParam 校验

@RestController
@Validated  // ⚠️ Controller类级别必须加此注解,否则单参数校验不生效
@RequestMapping("/api/user")
public class UserController {
    
    @GetMapping("/{id}")
    public Result getById(
        @PathVariable 
        @Min(value = 1, message = "ID必须大于0") Long id) {
        return Result.success(userService.getById(id));
    }
    
    @GetMapping("/check")
    public Result checkUsername(
        @RequestParam 
        @NotBlank(message = "用户名不能为空") 
        @Length(max = 50, message = "用户名最长50字符") String username) {
        return Result.success(userService.checkUsername(username));
    }
}

七、最佳实践

7.1 校验分组规范

com.example.validate.group/
├── AddGroup.java       # 新增操作校验组
├── UpdateGroup.java    # 更新操作校验组
├── DeleteGroup.java    # 删除操作校验组
└── QueryGroup.java     # 查询操作校验组

7.2 统一响应格式

public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
}

7.3 错误码规范

public enum ResultCodeEnum {
    SUCCESS(200, "操作成功"),
    PARAM_ERROR(400, "参数错误"),
    UNAUTHORIZED(401, "未授权"),
    FORBIDDEN(403, "禁止访问"),
    NOT_FOUND(404, "资源不存在"),
    INTERNAL_ERROR(500, "服务器内部错误");
    
    private final Integer code;
    private final String message;
    
    ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    
    public Integer getCode() { return code; }
    public String getMessage() { return message; }
}

7.4 快速失败模式(性能优化)

默认情况下,校验器会校验所有字段并收集所有错误。对于大量字段的DTO,可启用快速失败模式:

# application.yml 配置
hibernate:
  validator:
    fail_fast: true  # 遇到第一个错误即返回,不再继续校验
// 或通过代码配置
@Configuration
public class ValidatorConfig {
    
    @Bean
    public Validator validator() {
        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)  // 快速失败
                .buildValidatorFactory();
        return factory.getValidator();
    }
}

7.5 国际化消息配置

支持将校验消息提取到配置文件,便于多语言切换:

// DTO中使用消息Key
public class UserDTO {
    @NotBlank(message = "{user.name.notblank}")
    private String username;
    
    @Length(min = 2, max = 20, message = "{user.name.length}")
    private String nickname;
}
# ValidationMessages.properties(默认中文)
user.name.notblank=用户名不能为空
user.name.length=用户名长度必须在{min}到{max}之间

# ValidationMessages_en.properties(英文)
user.name.notblank=Username cannot be empty
user.name.length=Username length must be between {min} and {max}

注意:配置文件需放在 src/main/resources/ 目录下,文件名必须为 ValidationMessages.properties

7.6 DTO设计规范

// ✅ 推荐:使用分组校验复用DTO
public class UserDTO {
    @NotNull(groups = UpdateGroup.class)
    private Long id;
    
    @NotBlank(groups = {AddGroup.class, UpdateGroup.class})
    private String username;
    
    @NotBlank(groups = AddGroup.class)
    private String password;
}

// ✅ 推荐:查询参数使用单独DTO
public class UserQueryDTO {
    @Length(max = 50) 
    private String username;
    
    @Pattern(regexp = "^1[3-9]\\d{9}$") 
    private String phone;
    
    @Min(1) 
    private Integer pageNum;
    
    @Min(1) @Max(100) 
    private Integer pageSize;
}

// ❌ 不推荐:同一个类既做入参又做出参

八、代码合规检查表

8.1 开发阶段CheckList

检查项 是否合规 说明
☐ 必填字段有校验注解 @NotNull/@NotBlank/@NotEmpty
☐ 校验注解有明确message message = "用户名不能为空"
☐ 字符串字段用 @NotBlank 不是 @NotNull
☐ 分组校验指定groups 否则分组模式下不生效
☐ Controller参数有 @Valid/@Validated 无注解则校验不生效
☐ 单参数校验Controller类有 @Validated 否则 @RequestParam/@PathVariable 校验不生效
☐ 嵌套对象有 @Valid 触发级联校验
☐ 全局异常处理器已配置 捕获校验异常并返回友好提示
☐ 自定义校验器处理null null应返回true,由其他注解处理

8.2 Code Review检查点

□ 是否存在未校验的必填参数?
□ 错误提示是否清晰友好?
□ 是否存在冗余的手动校验代码?
□ 分组校验是否正确使用?
□ 自定义校验器是否符合规范?
□ 校验注解的边界值是否合理?
□ 是否有潜在的SQL注入/XSS风险?

8.3 常见问题排查

问题现象 可能原因 解决方案
校验不生效 未加 @Valid/@Validated Controller参数添加注解
分组校验不生效 未指定groups或groups不匹配 检查groups配置
单参数校验不生效 Controller类未加 @Validated 类级别添加注解
嵌套对象校验不生效 未添加 @Valid 嵌套字段添加 @Valid
自定义校验不生效 未实现 ConstraintValidator 或未指定 validatedBy 检查校验器实现和注解配置
null值校验异常 自定义校验器未处理null isValid中null返回true

九、附录

9.1 参考资源

9.2 注解来源分类

来源 注解
JSR303标准 @NotNull, @Null, @AssertTrue, @AssertFalse, @Min, @Max, @DecimalMin, @DecimalMax, @Digits, @Size, @Past, @Future, @Pattern
JSR380新增 @NotBlank, @NotEmpty, @Positive, @PositiveOrZero, @Negative, @NegativeOrZero, @PastOrPresent, @FutureOrPresent
Hibernate扩展 @Length, @Email, @Range, @CreditCardNumber, @URL, @ScriptAssert, @ParameterScriptAssert

📌 核心原则:永远不要信任客户端传入的参数,所有入参必须校验!前后端双重校验,后端是最后一道防线。

posted @ 2026-05-11 10:53  flycloudy  阅读(1)  评论(0)    收藏  举报