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

浙公网安备 33010602011771号