Java API 规范 (JSR303) 定义了 Bean 校验的标准 validation-api
,但没有提供实现。hibernate validation
是对这个规范的实现,并增加了校验注解如 @Email、@Length 等。Spring Validation
是对 hibernate validation
的二次封装,用于支持 spring mvc 参数自动校验。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
如果spring-boot
版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator
依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖
对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:
POST
、PUT
请求,使用@RequestBody
传递参数
GET
请求,使用 @RequestParam
/ @PathVariable
传递参数
@RestController
// 代表需要参数验证,一定要加
@Validated
public class HelloController {
// @Min代表参数不能小于10
@RequestMapping(value = "/test1")
public Object m1(@RequestParam(value = "number") @Min(value = 10) Integer number) {
System.out.println(number);
return UUID.randomUUID() + "----" + number;
}
}
【测试】
http://localhost:8081/test1?number=9 参数为9即报错,异常为ConstraintViolationException
http://localhost:8081/test1?number=12 参数为12即正常
@RestController
@Validated
public class HelloController {
@RequestMapping(value = "/test2/{number}")
public Object m2(@PathVariable(value = "number") @Max(value = 20) String number) {
System.out.println(number);
return UUID.randomUUID() + "----" + number;
}
}
在实体类上生命效验字段
package com.h3c.entity;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
@Data
public class UserParam {
@NotNull
private String userName;
@Length(min = 6, max = 20, message = "长度范围为6~20")
private String account;
@Length(min = 6, max = 20)
private String password;
}
在方法参数上声明校验注解@Validated
@RestController
public class HelloController {
// 需要设置声明,@Valid和@Validated都可以
@PostMapping(value = "/test3")
public Object m3(@RequestBody @Validated UserParam param) {
System.out.println(param);
return UUID.randomUUID() + "----" + param;
}
}
参数错误会报异常MethodArgumentNotValidException
前面说过,如果校验失败,会抛出MethodArgumentNotValidException
或者ConstraintViolationException
异常。在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。
package com.h3c.exception;
import com.h3c.entity.Result;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestControllerAdvice
public class SystemExceptionHandler {
// MethodArgumentNotValidException异常可以获取到异常字段
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
ex.printStackTrace();
// 获取所有的错误字段
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
Map<String, String> map = new LinkedHashMap<>();
// 把错误信息存入一个map,然后返回
errors.forEach(item -> {
map.put(item.getField(), item.getDefaultMessage());
});
return Result.error(map);
}
@ExceptionHandler({ConstraintViolationException.class})
public Result handleConstraintViolationException(ConstraintViolationException ex) {
ex.printStackTrace();
return Result.error(ex.getMessage());
}
}
【POST请求参数异常返回示例】
【GET请求参数异常返回示例】
{"flag":false,"code":500,"message":"m1.number: 最小不能小于10","data":null}
https://www.cnblogs.com/jinzlblog/p/16635043.html 注解
注解 | 用法 | 适用类型 |
---|---|---|
@Null | 被注解的字段必须为空 | |
@NotNull | 被注解的字段必须不为空 | |
@NotBlank | 带注解的元素不能为null,并且必须至少包含一个非空白字符 | |
@NotEmpty | 带注解的元素不能为null也不能为空 | String(长度)集合(大小)数组(长度) |
@AssertTrue | 检查该字段必须为True | Boolean |
@AssertFalse | 检查该字段必须为False | Boolean |
@Min(value) | 被注解的字段必须大于等于指定的最小值 | |
@Max(value) | 被注解的字段必须小于等于指定的最大值 | |
@Negative | 带注解的元素必须是严格的负数(0被认为是无效值) | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@NegativeOrZero | 带注解的元素必须是严格的负数或0 | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@Positive | 带注解的元素必须是严格的正数(0被认为是无效值) | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@PositiveOrZero | 带注解的元素必须是严格的正数或0 | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@DecimalMin | 被注解的字段必须大于等于指定的最小值 | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@DecimalMax | 被注解的字段必须小于等于指定的最大值 | BigDecimal,BigInteger,byte,short,int,long及其包装类 |
@Size(min=,max=) | 被注解的字段的size必须在min和max之间,不需要判空 | 字符串、数组、集合 |
@Digits(integer, fraction) | 被注解的字段必须在指定范围内,整数部分长度小于integer,小数部分长度小于fraction | 字符串、数组、集合 |
@Past | 被注解的字段必须是一个过去的日期时间 | |
@PastOrPresent | 被注解的字段必须是过去的或现在的日期时间 | |
@Future | 被注解的字段必须是一个将来的日期时间 | |
@FutureOrPresent | 被注解的字段必须是现在的或将来的日期时间 | |
字符串必须是格式正确的电子邮件地址 | String | |
@Pattern(value) | 被注解的字段必须符合指定的正则表达式 |
在实际项目中,可能多个接口需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation
支持了分组校验的功能,专门用来解决这类问题。
还是上面的例子,比如保存 User 的时候,UserId 是可空的,但是更新 User 的时候,UserId 的值必须存在,其它字段的校验规则在两种情况下一样。这个时候就需要使用分组校验
简单点说:就是根据设置的条件来执行参数校验,其实就是一个判断,筛选设置了分组的参数进行校验,不过在这里叫做分组
【声明分组】
// 保存的时候校验分组
public interface InsertValidGroup {
}
// 更新的时候校验分组
public interface UpdateValidGroup {
}
【实体类】
需要注意的是,这里面只有参数Id设置了分组,其它参数没有设置则不会进行校验
@Data
public class UserParam {
// 代表在属于更新的时候,校验id
@NotNull(groups = UpdateValidGroup.class)
private Integer id;
@NotNull
private String userName;
@Length(min = 6, max = 20, message = "长度范围为6~20")
private String account;
@Length(min = 6, max = 20)
private String password;
}
【接口】
@RestController
@Validated
public class HelloController {
@PostMapping(value = "/test3")
public Object m3(@RequestBody @Validated(UpdateValidGroup.class) UserParam param) {
System.out.println(param);
return UUID.randomUUID() + "----" + param;
}
}
【测试】
请求体id为空,则出现异常,请求体id存在,则正常执行
【其余参数设置分组】
如上所示只有设置了分组的参数才会校验,那么其它的参数怎么办呢,只能挨个写
@Data
public class UserParam {
@NotNull(groups = UpdateValidGroup.class)
private Integer id;
// 其余的参数把全部的分组都设置上
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
private String userName;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Length(min = 6, max = 20, message = "长度范围为6~20")
private String account;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Length(min = 6, max = 20)
private String password;
}
当我们实体类中某个字段是对象,这种情况下,可以使用嵌套校验
@Data
public class UserParam {
@NotNull(groups = UpdateValidGroup.class)
private Integer id;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
private String userName;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Length(min = 6, max = 20, message = "长度范围为6~20")
private String account;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Length(min = 6, max = 20)
private String password;
// 可以针对对象参数校验
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Valid
private Job job;
// 可以针对集合对象参数校验
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
@Valid
private List<Job> jobs;
@Data
public static class Job {
@NotNull(groups = UpdateValidGroup.class)
@Min(value = 10, groups = UpdateValidGroup.class)
private Long jobId;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
private String jobName;
@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})
private String position;
}
}
【接口情况】
【参数类】
@Data
public class ValidList {
// 集合校验
@Valid
@NotNull
@Size(min = 3, max = 10)
private List<String> list;
}
【接口】
@RestController
@Validated
public class HelloController {
@PostMapping(value = "/test4")
public Object m4(@RequestBody @Validated ValidList list) {
System.out.println(list);
return UUID.randomUUID() + "----" + list;
}
}
【测试】
业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。
例如性别参数只能是0或者1,某个字段必须是{“aa”,“bb”,"cc}中的一个
【自定义注解】
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
// 这里需要注意标明校验类
@Constraint(validatedBy = {ProcessValidator.class})
public @interface CheckValid {
String message() default "数据错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
【校验实现类】
需要实现ConstraintValidator
接口,然后重写isValid()
,验证该参数必须属于集合内
public class ProcessValidator implements ConstraintValidator<CheckValid, String> {
private List<String> list = Arrays.asList("aa", "bb", "cc");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null) {
if (list.contains(value)) {
return true;
}
}
return false;
}
}
【实体类参数】
@Data
public class ForumParam {
// 通过正则表达式验证
@NotNull
@Pattern(regexp = "^(男|女){1}$")
private String sex;
// 通过自定义注解校验
@NotNull
@CheckValid
private String pms;
}
【接口】
@RestController
@Validated
public class HelloController {
@PostMapping(value = "/test4")
public Object m4(@RequestBody @Validated ForumParam param) {
System.out.println(param);
return UUID.randomUUID() + "----" + param;
}
}
【测试】
上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入 javax.validation.Validator 对象,然后再调用其 api。
@Autowired
private javax.validation.Validator globalValidator;
// 编程式校验
@PostMapping(value = "/test5")
public Object m5(@RequestBody UserParam param) {
Set<ConstraintViolation<UserParam>> set = validator.validate(param, UpdateValidGroup.class);
// 如果校验通过,set;否则,set包含未校验通过项
if (set.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
// 遍历出现异常的字段
for (ConstraintViolation<UserParam> violation : set) {
System.out.println(violation.getPropertyPath() + "---" + violation.getMessage());
}
}
System.out.println(param);
return UUID.randomUUID() + "----" + param;
}
Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。
package com.h3c.config;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
/**
* @version JDK11
* @author: wys4822
* @date: 2022年09月07日
*/
@Configuration
public class ValidConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
【测试】
在 Spring-MVC
框架中,RequestResponseBodyMethodProcessor
是用于解析 @RequestBody
标注的参数以及处理 @ResponseBody
标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法 resolveArgument()
中:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 根据请求体转换参数
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 执行参数校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
}
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数前面设置的注解,注解中肯定会包含@RequestBody
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 判断是否存在@Validated,通过一个工具类来实现
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if (validationHints != null) {
binder.validate(validationHints);
break;
}
}
}
需要声明的是,在接口参数前面增加校验注解,注解可以为@Validated
或者@Valid
,2个都可以
@Valid
注解判断存在即可通过判断,而@Validated
注解判断存在后,还需要尝试获取里面的分组校验
public abstract class ValidationAnnotationUtils {
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
@Nullable
public static Object[] determineValidationHints(Annotation ann) {
Class<? extends Annotation> annotationType = ann.annotationType();
String annotationName = annotationType.getName();
// 通过注解路径判断是否为@Valid,原因是因为该注解有很多重名的
if ("javax.validation.Valid".equals(annotationName)) {
return EMPTY_OBJECT_ARRAY;
}
// 判断@Validated注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 因为@Validated可以设置分组校验,所以这里需要获取value,封装成数组返回
if (validatedAnn != null) {
Object hints = validatedAnn.value();
return convertValidationHints(hints);
}
if (annotationType.getSimpleName().startsWith("Valid")) {
Object hints = AnnotationUtils.getValue(ann);
return convertValidationHints(hints);
}
return null;
}
private static Object[] convertValidationHints(@Nullable Object hints) {
if (hints == null) {
return EMPTY_OBJECT_ARRAY;
}
return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
}
}
上面验证了参数是否标注了@Validated
或者@Valid
,代表该参数需要执行校验逻辑,那么接下来肯定就是遍历字段校验了,那么该validate()
在上面的validateIfApplicable
被调用
public void validate(Object... validationHints) {
// 获取到参数对象
Object target = getTarget();
// 参数为空,就报错了
Assert.state(target != null, "No target to validate");
// 获取默认的绑定结果
BindingResult bindingResult = getBindingResult();
// 遍历校验
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
// 开启核心的校验
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
{
// 获取到参数对象
Object target = getTarget();
// 参数为空,就报错了
Assert.state(target != null, "No target to validate");
// 获取默认的绑定结果
BindingResult bindingResult = getBindingResult();
// 遍历校验
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
// 开启核心的校验
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}