Bean Validation ( JSR 303 规范)
Hibernate Validator
是Jakarta Bean Validation
(即 JSR 303)的参考实现。
- 在开发中,从表现层到持久层,数据校验都是一项逻辑差不多,但容易出错的任务
教程网址:https://www.cnblogs.com/summerday152/p/13984576.html
-
前端架构往往会采取一些检查参数的手段,比如校验并提示信息,那么,既然前端已经存在校验手段,后端的校验是否还有必要,是否多余了呢 ?
-
并不是,正常情况下,参数确实会经过前端校验后传向后端,但如果后端不做校验,一但通过特殊手段越过前端的检测,系统就会出现漏洞。
-
不使用 Validator 的参数处理逻辑,if / else 就能搞定,写法干脆,但 if / else 太多,过于臃肿,更何况这只是区区一个接口的两个参数而已,要是需要更多参数校验,甚至更多方法都需要的校验,这代码量可想而知。于是,这种做法显然是不可取的,我们可以利用 Validator 框架更加优雅的处理参数。
-
Jakarta Bean Validation2.0 定义了一个元数据模型,为实体类和方法提供了数据验证的 API ,默认将注解作为源,可以通过 XML 扩展源。
-
SpringBoot 自动配置 ValidationAutoConfiguration
-
Hibernate Validator
是Jakarta Bean Validation
的参考实现。 -
在 SpringBoot 中,只要类路径上存在 JSR-303 的实现,如 Hibernate Validator,就会自动开启 Bean Validation 验证功能,这李我们只要引入
spring-boot-starter-validation
的依赖,就能完成所需。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
简单实现步骤:
- 引入上面依赖
- 创建 Bean, Bean 的属性和前端的请求参数一致,在 Bean 的属性加入注解,注解提供了数据验证的规则
- 在 Controller 中,形参是 Bean 对象,使用 @Validated 说明这个 Bean 要进行属性验证
- 使用 Spring Boot 异常处理机制,获取验证的结果
SpringBoot Validtor 常用注解
@AssertFalse | Boolean,boolean | 验证注解的元素值是false |
---|---|---|
@AssertTrue | Boolean,boolean | 验证注解的元素值是true |
@NotNull | 任意类型 | 验证注解的元素值不是null |
@Null | 任意类型 | 验证注解的元素值是null |
@Min(value=值) | BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型 | 验证注解的元素值大于等于@Min指定的value值 |
@Max(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@Max指定的value值 |
@DecimalMin(value=值) | 和@Min要求一样 | 验证注解的元素值大于等于@ DecimalMin指定的value值 |
@DecimalMax(value=值) | 和@Min要求一样 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样 | 验证注解的元素值的整数位数和小数位数上限 |
@Size(min=下限, max=上限) | 字符串、Collection、Map、数组等 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 | 验证注解的元素值(日期类型)比当前时间早 |
@Future | 与@Past要求一样 | 验证注解的元素值(日期类型)比当前时间晚 |
@NotBlank | CharSequence子类型 | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格 |
@Length(min=下限, max=上限) | CharSequence子类型 | 验证注解的元素值长度在min和max区间内 |
@NotEmpty | CharSequence子类型、Collection、Map、数组 | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Range(min=最小值, max=最大值) | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型 | 验证注解的元素值在最小值和最大值之间 |
@Email(regexp=正则表达式,flag=标志的模式) | CharSequence子类型(如String) | 验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式 |
@Pattern(regexp=正则表达式,flag=标志的模式) | String,任何CharSequence的子类型 | 验证注解的元素值与指定的正则表达式匹配 |
@Valid | 任何非原子类型 | 指定递归验证关联的对象;如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证 |
下面是网站详细步骤
目的其实是为了引入如下依赖:
<!-- Unified EL 获取动态表达式-->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
<scope>compile</scope>
</dependency>
SpringBoot 对 BeanValidation 的支持的自动装配定义在:
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration 类中,
提供了默认的 LocalValidatorFactoryBean
和支持方法级别的拦截器 MethodValidationPostProcessor
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
//ValidatorFactory
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
// 支持Aop,MethodValidationInterceptor方法级别的拦截器
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
// factory.getValidator(); 通过factoryBean获取了Validator实例,并设置
processor.setValidator(validator);
return processor;
}
}
Validator+BindingResult优雅处理
为实体类定义约束注解
/**
* 实体类字段加上javax.validation.constraints定义的注解
* @author Summerday
*/
@Data
@ToString
public class Person {
private Integer id;
@NotNull
@Size(min = 6,max = 12)
private String name;
@NotNull
@Min(20)
private Integer age;
}
使用@Valid或@Validated注解
-
@Valid和@Validated在Controller层做方法参数校验时功能相近,具体区别可以往后面看
@RestController public class ValidateController { @PostMapping("/person") public Map<String, Object> validatePerson(@Validated @RequestBody Person person, BindingResult result) { 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里 } }
Validator + 全局异常处理
-
在接口方法中利用 BindingResult 处理校验数据过程中的信息是一个可行方案,但在接口众多的情况下,就显得有些冗余,我们可以利用全局异常处理,捕捉抛出的 MethodArgumentNotValidException (参数无效异常)异常,并进行相应的处理。
定义全局异常处理
@RestControllerAdvice public class GlobalExceptionHandler { /** * If the bean validation is failed, it will trigger a MethodArgumentNotValidException. 如果出现异常进不去全局异常可以都写成 BindException 这时下面异常的父类,也可能是抛出的异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpStatus status) { BindingResult result = ex.getBindingResult(); Map<String, Object> map = new HashMap<>(); List<String> list = new LinkedList<>(); result.getFieldErrors().forEach(error -> { String field = error.getField(); Object value = error.getRejectedValue(); String msg = error.getDefaultMessage(); list.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s", field, value, msg)); }); map.put("msg", list); return new ResponseEntity<>(map, status); } }
Validated 精确校验到参数字段
-
有时候,我们只想校验某个字段,并不想校验整个 POJO 对象,我们可以利用 @Validated 精确校验到某个字段。
-
定义接口
@RestController @Validated public class OnlyParamsController { @GetMapping("/{id}/{name}") public String test(@PathVariable("id") @Min(1) Long id, @PathVariable("name") @Size(min = 5, max = 10) String name) { return "success"; } }
-
当校验生效,但是状态和响应错误信息不太正确,我们可以通过捕获 ConstraintViolationException 修改状态
@ControllerAdvice public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final Logger log = LoggerFactory.getLogger(CustomGlobalExceptionHandler.class); /** * If the @Validated is failed, it will trigger a ConstraintViolationException */ @ExceptionHandler(ConstraintViolationException.class) public void constraintViolationException(ConstraintViolationException ex, HttpServletResponse response) throws IOException { ex.getConstraintViolations().forEach(x -> { String message = x.getMessage(); Path propertyPath = x.getPropertyPath(); Object invalidValue = x.getInvalidValue(); log.error("错误字段 -> {} 错误值 -> {} 原因 -> {}", propertyPath, invalidValue, message); }); response.sendError(HttpStatus.BAD_REQUEST.value()); } }
@Validated 和 @Valid 的不同
-
@Valid :是标准 JSR-303 规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验。
-
@Validated:是 Spring 提供的注解,是标准 JSR-303 的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采取不同的验证机制。
-
在 Controller 中校验方法参数时,使用 @Valid 和 @Validated 并无特殊差异(若不需要分组校验的话)。
-
@Validated 注解可以用于类级别,用于支持 Spring 进行方法级别的参数校验,@Valid 可以用在属性级别约束,用来表示级联校验
-
@Validated 只能用在类,方法和参数上,而 @Valid 可以用于方法,字段,构造器和参数上。
自定义 Jakarta Bean Validation API 注解
-
Jakarta Bean Validation API 定义了一套标准约束注解,如 @NotNull,@Size等,但是这些内置的约束注解难免不能满足我们的需求,这时我们就可以自定义注解,创建自定义注解需要三步:
-
创建一个 Constraint annotation
-
实现一个 Validator
-
定义一个 default error message
第一步:
/**
* 自定义注解
* @author Summerday
*/
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE, TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class) //需要定义CheckCaseValidator
@Documented
@Repeatable(CheckCase.List.class)
public @interface CheckCase {
String message() default "{CheckCase.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CheckCase[] value();
}
}
第二步
/**
* 实现ConstraintValidator
*
* @author Summerday
*/
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
/**
* 初始化获取注解中的值
*/
@Override
public void initialize(CheckCase constraintAnnotation) {
this.caseMode = constraintAnnotation.value();
}
/**
* 校验
*/
@Override
public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
if (object == null) {
return true;
}
boolean isValid;
if (caseMode == CaseMode.UPPER) {
isValid = object.equals(object.toUpperCase());
} else {
isValid = object.equals(object.toLowerCase());
}
if (!isValid) {
// 如果定义了message值,就用定义的,没有则去
// ValidationMessages.properties中找CheckCase.message的值
if(constraintContext.getDefaultConstraintMessageTemplate().isEmpty()){
constraintContext.disableDefaultConstraintViolation();
constraintContext.buildConstraintViolationWithTemplate(
"{CheckCase.message}"
).addConstraintViolation();
}
}
return isValid;
}
}
第三步:在 ValidationMessages.properties 文件中定义
CheckCase.message=Case mode must be {value}.
第四步:在某个字段上加上注解:
@CheckCase(value = CaseMode.UPPER)