一、依赖引入
Spring Boot 提供的 spring-boot-starter-validation 依赖整合了 JSR-380 规范(Bean Validation 2.0)及 Hibernate Validator 实现,支持便捷的请求参数校验功能,无需手动编写重复的校验逻辑。
在项目 pom.xml 中引入依赖(Spring Boot 2.3+ 需显式引入,2.3 以下版本可通过 spring-boot-starter-web 间接依赖):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<!-- 无需指定版本,Spring Boot 父工程已统一管理 -->
</dependency>
二、核心使用步骤(对象参数校验)
Spring MVC 中最常用的校验场景是对象类型的请求参数校验,需遵循「注解标记→对象定义→异常处理」三步法:
步骤 1:Controller 方法标记 @Valid
在接收的对象参数前添加 @Valid 注解(或 @Validated),告知 Spring MVC 对该参数执行校验:
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/test")
// 类上添加 @Validated 可支持方法参数(非对象)的校验
@Validated
public class TestController {
/**
* 测试对象参数校验
* @param req 待校验的请求对象
* @param request 请求上下文
* @return 响应结果
*/
@RequestMapping(value = "/req.json")
public Object test(@Valid TestRequest req, HttpServletRequest request) {
// 校验通过后才会执行此处业务逻辑
return "请求成功,req=" + req.getReq();
}
/**
* 扩展:基本类型参数校验(需类上添加 @Validated)
* 校验不通过会抛出 ConstraintViolationException
*/
@RequestMapping(value = "/base.json")
public Object testBaseParam(
@NotBlank(message = "用户名不能为空") String username,
@Min(value = 18, message = "年龄不能小于18岁") Integer age) {
return "用户名:" + username + ",年龄:" + age;
}
}
步骤 2:定义请求对象并添加校验注解
创建请求参数对应的实体类,在需要校验的字段上添加「常用校验注解」,并指定错误提示信息:
import javax.validation.constraints.NotBlank;
public class TestRequest {
// @NotBlank:字符串不能为 null 且去除首尾空格后长度 > 0
@NotBlank(message = "req参数不能为空(不能是空白字符)")
private String req;
// 必须提供 getter/setter,否则 Spring MVC 无法注入参数
public String getReq() {
return req;
}
public void setReq(String req) {
this.req = req;
}
}
步骤 3:全局异常处理器统一处理校验异常
校验不通过时,Spring MVC 会抛出不同类型的异常(对象参数抛出 BindException/MethodArgumentNotValidException,基本类型参数抛出 ConstraintViolationException),需通过「全局异常处理器」捕获并统一返回格式化响应:
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.annotation.Resource;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Locale;
import java.util.Set;
/**
* 全局异常处理器:统一处理参数校验异常
*/
@RestControllerAdvice
public class GlobalValidationExceptionHandler {
// 用于国际化消息解析(可选)
@Resource
private MessageSource messageSource;
/**
* 处理对象参数校验异常(@Valid 标记的对象)
* 包括:BindException(表单提交)、MethodArgumentNotValidException(JSON 提交)
*/
@ExceptionHandler({BindException.class, MethodArgumentNotValidException.class})
public Result<?> handleObjectValidationException(Exception ex) {
ObjectError objectError = null;
// 区分不同的对象校验异常类型
if (ex instanceof BindException) {
// 表单提交(application/x-www-form-urlencoded)
objectError = ((BindException) ex).getBindingResult().getAllErrors().get(0);
} else if (ex instanceof MethodArgumentNotValidException) {
// JSON 提交(application/json)
objectError = ((MethodArgumentNotValidException) ex).getBindingResult().getAllErrors().get(0);
}
// 解析错误信息(支持国际化)
Locale locale = LocaleContextHolder.getLocale();
String errorMsg = messageSource.getMessage(objectError, locale);
return Result.error(400, "参数校验失败", errorMsg);
}
/**
* 处理基本类型/单个参数校验异常(@Validated 标记的类)
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleBaseParamValidationException(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
// 获取第一个错误信息(也可收集所有错误)
String errorMsg = violations.iterator().next().getMessage();
return Result.error(400, "参数校验失败", errorMsg);
}
// 通用响应类(简化示例)
static class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> error(int code, String msg, T data) {
Result<T> result = new Result<>();
result.code = code;
result.msg = msg;
result.data = data;
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.code = 200;
result.msg = "操作成功";
result.data = data;
return result;
}
// getter/setter 省略
}
}
三、常用校验注解详解
JSR-380 规范定义了一系列标准校验注解,Hibernate Validator 扩展了部分注解,以下是开发中最常用的注解及使用场景:
| 注解名称 | 核心作用 | 适用类型 | 关键属性说明 |
|---|---|---|---|
| @NotBlank | 字符串不能为 null 且去除首尾空格后长度 > 0 | String | message:错误提示 |
| @NotNull | 值不能为 null(不校验空字符串、空集合) | 所有类型(对象、基本类型包装类) | - |
| @NotEmpty | 集合 / 数组 / 字符串不能为 null 且长度 > 0(字符串不忽略首尾空格) | String、Collection、Map、数组 | - |
| @Min(value) | 数字不能小于 value(不支持 float/double,避免精度问题) | 数值类型(Integer、Long 等) | value:最小值;inclusive:是否包含最小值(默认 true) |
| @Max(value) | 数字不能大于 value(不支持 float/double) | 数值类型 | 同 @Min |
| @DecimalMin(value) | 支持小数的最小值校验(可指定数值格式) | 数值类型、String | value:最小值(支持小数);inclusive:是否包含最小值 |
| @DecimalMax(value) | 支持小数的最大值校验 | 数值类型、String | 同 @DecimalMin |
| 字符串必须符合邮箱格式(支持自定义正则) | String | regexp:自定义邮箱正则;flags:正则匹配模式 | |
| @Pattern(regexp) | 字符串必须匹配指定正则表达式 | String | regexp:正则表达式;flags:匹配模式(如 CASE_INSENSITIVE 忽略大小写) |
| @Size(min, max) | 集合 / 数组 / 字符串的长度在 [min, max] 范围内 | String、Collection、Map、数组 | min:最小长度;max:最大长度(默认 Integer.MAX_VALUE) |
| @Future | 日期必须是当前时间之后的时间 | Date、LocalDateTime 等 | - |
| @FutureOrPresent | 日期必须是当前时间或之后的时间 | 日期类型 | - |
| @Past | 日期必须是当前时间之前的时间 | 日期类型 | - |
| @PastOrPresent | 日期必须是当前时间或之前的时间 | 日期类型 | - |
| @Positive | 数字必须是正数(> 0) | 数值类型 | - |
| @PositiveOrZero | 数字必须是正数或 0(≥ 0) | 数值类型 | - |
| @Negative | 数字必须是负数(< 0) | 数值类型 | - |
| @NegativeOrZero | 数字必须是负数或 0(≤ 0) | 数值类型 | - |
| @Digits(integer, fraction) | 数字的整数部分位数 ≤ integer,小数部分位数 ≤ fraction | 数值类型、String | integer:整数最大位数;fraction:小数最大位数 |
注解使用示例
import javax.validation.constraints.*;
import java.time.LocalDateTime;
public class UserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String username;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 60, message = "年龄不能大于60岁")
private Integer age;
@Email(message = "邮箱格式不正确", regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$")
private String email;
@Past(message = "生日必须是过去的时间")
private LocalDateTime birthday;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@DecimalMin(value = "0.01", message = "金额不能小于0.01")
@DecimalMax(value = "10000.00", message = "金额不能大于10000.00")
private Double amount;
// getter/setter 省略
}
四、自定义校验注解(扩展能力)
当默认校验注解无法满足业务需求时(如「手机号格式校验」「自定义状态值校验」),可通过 JSR-380 规范提供的扩展机制实现自定义校验注解。
实现步骤(以「手机号校验」为例):
步骤 1:创建自定义校验注解
注解必须标注 @Constraint 并指定对应的验证器,同时包含 message、groups、payload 三个必填属性(JSR-380 规范要求):
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 自定义手机号校验注解
*/
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 支持字段和方法参数
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
@Constraint(validatedBy = PhoneValidator.class) // 关联自定义验证器
public @interface Phone {
// 错误提示信息(支持国际化,默认值可引用配置文件)
String message() default "手机号格式不正确(必须是11位有效手机号)";
// 校验分组(用于多场景校验,如新增/编辑不同规则)
Class<?>[] groups() default {};
// 负载信息(用于传递额外校验元数据)
Class<? extends Payload>[] payload() default {};
}
步骤 2:实现 ConstraintValidator 接口
创建验证器类,实现 ConstraintValidator<A, T> 接口(A 为自定义注解,T 为校验目标类型),核心逻辑在 isValid 方法中:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* 手机号校验器:实现 ConstraintValidator 接口
*/
public class PhoneValidator implements ConstraintValidator<Phone, String> {
// 手机号正则表达式(支持13-9开头的11位数字)
private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
/**
* 初始化方法:可获取注解的属性值(如自定义正则)
*/
@Override
public void initialize(Phone constraintAnnotation) {
// 若注解有自定义属性(如 regexp),可在此处获取并初始化
ConstraintValidator.super.initialize(constraintAnnotation);
}
/**
* 校验核心方法:返回 true 表示校验通过,false 表示失败
* @param value 待校验的值(手机号字符串)
* @param context 校验上下文(可用于自定义错误信息)
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 1. 允许值为 null(若不允许 null,需配合 @NotNull 注解)
if (value == null) {
return true;
}
// 2. 正则匹配校验
return PHONE_PATTERN.matcher(value).matches();
}
}
步骤 3:使用自定义注解
与默认注解用法完全一致,可直接标注在字段或参数上:
public class UserRequest {
@Phone(message = "手机号格式错误,请输入11位有效手机号")
private String phone;
// 配合 @NotNull 注解,禁止手机号为 null
@NotNull(message = "手机号不能为空")
@Phone
private String requiredPhone;
// getter/setter 省略
}
五、进阶使用技巧
1. 分组校验
当同一个对象在不同场景(如「新增用户」和「编辑用户」)有不同校验规则时,可通过「分组校验」实现:
// 1. 定义分组接口(无需实现)
public interface AddGroup {}
public interface UpdateGroup {}
// 2. 注解指定分组
public class UserRequest {
@NotNull(groups = AddGroup.class, message = "新增时ID不能为空")
@Null(groups = UpdateGroup.class, message = "编辑时ID必须为空")
private Long id;
@NotBlank(groups = {AddGroup.class, UpdateGroup.class}, message = "用户名不能为空")
private String username;
}
// 3. Controller 指定分组校验
@RestController
@RequestMapping("/user")
public class UserController {
// 新增用户:只校验 AddGroup 分组的注解
@PostMapping("/add")
public Result<?> add(@Validated(AddGroup.class) UserRequest request) {
return Result.success("新增成功");
}
// 编辑用户:只校验 UpdateGroup 分组的注解
@PostMapping("/update")
public Result<?> update(@Validated(UpdateGroup.class) UserRequest request) {
return Result.success("编辑成功");
}
}
2. 嵌套校验
当对象中包含另一个对象属性,且需要对嵌套对象进行校验时,需在嵌套对象字段上添加 @Valid 注解:
public class UserRequest {
@NotBlank(message = "用户名不能为空")
private String username;
// 嵌套对象校验:必须添加 @Valid 注解
@Valid
@NotNull(message = "地址信息不能为空")
private AddressRequest address;
// 嵌套对象类
public static class AddressRequest {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
// getter/setter 省略
}
// getter/setter 省略
}
3. 国际化错误提示
将错误提示信息存入国际化配置文件,通过 MessageSource 解析:
# src/main/resources/messages.properties(默认)
user.username.notBlank=用户名不能为空
user.phone.invalid=手机号格式不正确
# src/main/resources/messages_zh_CN.properties(中文)
user.username.notBlank=用户名不能为空
user.phone.invalid=手机号格式不正确
# src/main/resources/messages_en_US.properties(英文)
user.username.notBlank=Username cannot be blank
user.phone.invalid=Phone number format is invalid
使用时引用配置文件中的 key:
public class UserRequest {
@NotBlank(message = "{user.username.notBlank}")
private String username;
@Phone(message = "{user.phone.invalid}")
private String phone;
}
六、常见问题与注意事项
1. @Valid 与 @Validated 的区别:
- @Valid:JSR-380 标准注解,支持对象校验、嵌套校验,不支持分组校验和方法参数(非对象)校验。
- @Validated:Spring 扩展注解,支持分组校验、方法参数(非对象)校验,不支持嵌套校验(需配合 @Valid)。
2. 基本类型参数校验失败:
- 需在 Controller 类上添加 @Validated 注解,否则 MethodValidationPostProcessor 无法拦截方法。
- 校验失败会抛出 ConstraintViolationException,需在全局异常处理器中单独处理。
3. JSON 提交与表单提交的异常差异:
- JSON 提交(Content-Type: application/json):校验失败抛出 MethodArgumentNotValidException。
- 表单提交(Content-Type: application/x-www-form-urlencoded):校验失败抛出 BindException。
4. float/double 类型的数值校验:
- 避免使用 @Min/@Max,因浮点型精度问题可能导致校验失效,建议使用 @DecimalMin/@DecimalMax 或转换为 String 类型后用 @Pattern 校验。
5. 自定义注解不生效:
- 确保注解标注了 @Constraint 并指定了 validatedBy 属性。
- 验证器类必须实现 ConstraintValidator 接口,且泛型与注解、目标类型一致。
- 注解的 Retention 必须为 RUNTIME(运行时才能被反射获取)。
8140

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



