1 引入依赖
Spring Boot 3.x 也使用这个 starter,但包名迁移到了 jakarta.validation。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>2 在 DTO 上加注解
publicclassUserDTO{@NotBlank(message="用户名不能为空")privateStringusername;@Email(message="邮箱格式不正确")privateStringemail;@NotNull(message="年龄不能为空")@Min(value=0,message="年龄不能小于0")privateIntegerage;}3 @Valid触发默认校验
@PostMapping("/users")publicResultaddUser(@Valid@RequestBodyUserDTOuser){returnResult.success("添加成功");}4 @Validated触发分组校验
4.1 定义分组接口
publicinterfaceAddGroup{}publicinterfaceUpdateGroup{}4.2 在 DTO 上用 groups 指定分组
注意:
- 没写 groups 的注解,属于 默认分组 Default.class。
- 如果写了 groups,它只属于指定分组,默认分组不会触发。
publicclassUserDTO{// 新增和修改都需要 id@NotNull(message="id不能为空",groups={UpdateGroup.class})privateLongid;@NotBlank(message="用户名不能为空",groups={AddGroup.class,UpdateGroup.class})privateStringusername;@Email(message="邮箱格式不正确",groups={AddGroup.class})privateStringemail;@NotNull(message="年龄不能为空",groups={AddGroup.class,UpdateGroup.class})@Min(value=0,message="年龄不能小于0",groups={AddGroup.class,UpdateGroup.class})privateIntegerage;}4.3 方法参数上用 groups 指定分组
@PostMapping("/add")publicRadd(@Validated(AddGroup.class)@RequestBodyOrderDTOorder){...}5 @Valid和@Validated区别
参考文章:别再混淆了!一文带你搞懂@Valid和@Validated的区别
| 维度 | @Valid | @Validated |
|---|---|---|
| 来源 | JSR 303/380 标准,javax.validation/jakarta.validation | Spring 扩展,org.springframework.validation.annotation |
| 分组校验 | ❌ 不支持 | ✅ 支持,@Validated(AddGroup.class) |
| 嵌套校验 | ✅ 支持,可标在字段上触发子对象校验 | ❌ 不支持,不能标在字段上 |
| 可用位置 | 方法参数、成员变量(递归校验)、构造函数参数 | 方法参数、方法、类 (开启该类校验) |
| 校验失败异常 | MethodArgumentNotValidException | MethodArgumentNotValidException(Controller 参数);ConstraintViolationException(Service 层 AOP) |
5.1 类上的 @Validated:开启方法级校验
下面情况 类上不用添加@Validated开启方法校验,默认触发校验。
| 场景 | 情况 |
|---|---|
@RequestBody对象参数 | @Valid @RequestBody会触发 DTO 内部校验 |
@ModelAttribute/ 表单绑定对象 | 表单提交、查询参数绑定到对象时,@Valid也能触发校验 |
@RequestParam、@PathVariable、Service 方法参数 | 这些不是@RequestBody,@Valid放在参数上通常不会自动生效;需要类上加@Validated走方法级校验 |
下面情况 类上必须加@Validated开启方法校验,否则@NotBlank校验不生效
@RestController@RequestMapping("/brand")@Validated// 关键:类上必须加 @ValidatedpublicclassBrandController{/** * 按品牌名称查询 */@GetMapping("/search")publicRsearch(@RequestParam("name")@NotBlank(message="品牌名称不能为空")Stringname){returnR.ok();}}5.2 方法参数上的@Valid/@Validated:分组与嵌套
@Validated可以实现分组校验,@Valid无此功能。 @Valid可以实现嵌套校验,@Validated不能触发。public R save(@Valid @RequestBody BrandEntity brand)
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand)
嵌套校验举例
publicclassBrandEntity{privateStringname;// 嵌套对象privateAddressEntityaddress;}- 如果你用 @Valid:Spring 不仅会校验 BrandEntity 的 name,还会自动进入 AddressEntity 里面,去校验它的字段。
- 如果你用@Validated不会进入 AddressEntity 里面。
- 如果想要同时实现分组校验和嵌套校验,@Validated修饰方法参数并在AddressEntity嵌套对象上添加@Valid。
5.3 成员变量上的@Valid:嵌套
@Valid private Address address;,这是它和 @Validated 的关键区别之一
开启嵌套校验
publicclassUserDTO{@NotBlank(message="姓名不能为空")privateStringname;@NotNull(message="地址不能为空")@Valid// 加上 @Valid:会递归校验嵌套对象privateAddressDTOaddress;}所以@Validated修饰的方法参数内部,可以在成员private AddressDTO address上添加@Valid,实现分组校验和嵌套校验。
6 异常处理
6.1全局异常处理
参考文章:SpringBoot 使用Spring Validation实现接口参数校验全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可。
- @RestControllerAdvice本质上是@Component,可以注入全局异常处理bean。配置basePackages参数,处理指定包下抛出的异常。如果不指定,则会处理所有的@Controller标注的类抛出的异常。
- @ExceptionHandler 标注异常处理方法,其value标注要处理的异常类型,一般配置一个value = Exception.class处理其他未配置异常类型
@RestControllerAdvice(basePackages="com.atguigu.gulimall.product.controller")publicclassGulimallExceptionControllerAdvice{@ExceptionHandler(value=MethodArgumentNotValidException.class)publicRhandleValidException(MethodArgumentNotValidExceptione){BindingResultbindingResult=e.getBindingResult();HashMap<String,String>erroMap=newHashMap<>();bindingResult.getFieldErrors().forEach((fieldError->{erroMap.put(fieldError.getField(),fieldError.getDefaultMessage());}));returnR.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data",erroMap);}@ExceptionHandler(value=Exception.class)publicRhandleException(Exceptionexception){returnR.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());}}6.2 BingResult
BindingResult 是 Spring MVC 里用来接收参数校验结果的对象。校验失败时,它会把错误信息存起来,而不会直接抛异常。
它必须紧跟在被 @Valid 或 @Validated 标注的参数后面
@PostMapping("/user")publicStringcreateUser(@Valid@RequestBodyUseruser,BindingResultresult){if(result.hasErrors()){Stringmsg=result.getFieldErrors().stream().map(e->e.getField()+": "+e.getDefaultMessage()).collect(Collectors.joining("; "));returnResult.fail(400,msg);}...}| 方法 | 作用 |
|---|---|
hasErrors() | 是否有校验错误 |
hasFieldErrors() | 是否有字段级别的错误 |
getFieldErrors() | 获取所有字段错误列表 |
getFieldError("name") | 获取指定字段的第一个错误 |
getAllErrors() | 获取所有错误(含全局错误) |
getErrorCount() | 错误总数 |
7 自定义校验注解
7.1 自定义注解@Phone
@Documented@Constraint(validatedBy={PhoneValidator.class})@Target({ElementType.FIELD,ElementType.PARAMETER})@Retention(RetentionPolicy.RUNTIME)public@interfacePhone{Stringmessage()default"手机号格式校验不正确";Class<?>[]groups()default{};Class<?extendsPayload>[]payload()default{};StringphoneReg()default"^1[3-9]\\d{9}$";}@Constraint(validatedBy = PhoneValidator.class):指定7.2 注解校验处理方法PhoneValidator
这里三个属性是 Bean Validation 规范要求保留的:
| 属性 | 作用 |
|---|---|
message() | 校验失败时的提示信息 |
groups() | 分组校验使用 |
payload() | 附加元数据,一般默认即可 |
7.2 注解校验处理方法
publicclassPhoneValidatorimplementsConstraintValidator<Phone,String>{privateStringregPhone;@Overridepublicvoidinitialize(PhoneconstraintAnnotation){//获取注解中phoneReg的值StringphoneReg=constraintAnnotation.phoneReg();this.regPhone=phoneReg;}@OverridepublicbooleanisValid(Stringvalue,ConstraintValidatorContextcontext){//value匹配正则表达式,返回是否符合//为空时交由其他校验(如@NotBlank)处理,此处直接视为通过if(value==null||value.isEmpty()){returntrue;}returnvalue.matches(this.regPhone);}}ConstraintValidator<A, T>的两个泛型含义是:
A:自定义注解类型。
T:被校验字段的数据类型。isValid(String value, ConstraintValidatorContext context)两个参数含义:
value:当前正在被校验的那个值。
context:校验上下文对象,用来控制“校验失败后怎么报错”。修改错误提示的message消息。isValid方法返回布尔类型,代表校验通过与否。
7.3 使用
· DTO层
@DatapublicclassPhoneDTO{@NotNull(message="姓名不能为空")privateStringname;@Phone(phoneReg="^15[3-9]\\d{8}$",message="请填写15开头的手机号码")privateStringphone;}· Controller层
@RestController@RequestMapping("product/category")publicclassCategoryController{@RequestMapping("/test/phone")publicRtestPhone(@Valid@RequestBodyPhoneDTOphone,BindingResultbindingResult){StringerrorMessage=null;if(bindingResult.hasErrors()){errorMessage=bindingResult.getFieldErrors().get(0).getDefaultMessage();returnR.error(400,"参数错误").put("errors",errorMessage);}returnR.ok().put("page",errorMessage);}}7.4 Http测试
POST http://localhost:10001/product/category/test/phone Content-Type: application/json { "name": "test", "phone": "1381234567" }8 常见的Validation注解
8.1 空值校验
| 注解 | 适用类型 | 校验规则 | 用例 |
|---|---|---|---|
@NotNull | 任意类型 | 不能为null,但可以是空字符串、空集合 | @NotNull(message="id不能为空") private Long id; |
@NotBlank | String | 不能为null,且去除首尾空格后长度必须大于 0 | @NotBlank(message="用户名不能为空") private String username; |
@NotEmpty | String、集合、数组、Map | 不能为null,且长度/元素个数大于 0 | @NotEmpty(message="标签不能为空") private List<String> tags; |
简单区分:
字符串必填优先用 @NotBlank;集合/数组必填用 @NotEmpty;非字符串对象、数字、日期用 @NotNull。
8.2 字符串格式与长度
| 注解 | 适用类型 | 校验规则 | 用例 |
|---|---|---|---|
@Min | 数值类型 | 必须 ≥ 指定值 | @Min(18) private Integer age; |
@Max | 数值类型 | 必须 ≤ 指定值 | @Max(120) private Integer age; |
@DecimalMin | 数值类型、字符串数字 | 必须 ≥ 指定值,适合金额、小数 | @DecimalMin("0.00") private BigDecimal price; |
@DecimalMax | 数值类型、字符串数字 | 必须 ≤ 指定值 | @DecimalMax("99999.99") private BigDecimal price; |
@Positive | 数值类型 | 必须大于 0 | @Positive private Integer count; |
@PositiveOrZero | 数值类型 | 必须 ≥ 0 | @PositiveOrZero private BigDecimal amount; |
@Negative | 数值类型 | 必须小于 0 | @Negative private Integer loss; |
@NegativeOrZero | 数值类型 | 必须 ≤ 0 | @NegativeOrZero private BigDecimal change; |
@Digits | 数值类型 | 限制整数位数和小数位数 | @Digits(integer=10, fraction=2) private BigDecimal amount; |
8.3 日期时间校验
| 注解 | 适用类型 | 校验规则 | 用例 |
|---|---|---|---|
@Past | 日期时间 | 必须是过去时间 | @Past private LocalDate birthday; |
@PastOrPresent | 日期时间 | 必须是过去或当前时间 | @PastOrPresent private LocalDate startDate; |
@Future | 日期时间 | 必须是未来时间 | @Future private LocalDate expireDate; |
@FutureOrPresent | 日期时间 | 必须是未来或当前时间 | @FutureOrPresent private LocalDateTime endTime; |
8.4 布尔校验
| 注解 | 适用类型 | 校验规则 | 用例 |
|---|---|---|---|
@AssertTrue | Boolean | 必须为true | @AssertTrue private Boolean agreed; |
@AssertFalse | Boolean | 必须为false | @AssertFalse private Boolean deleted; |