Spring Validation参数效验各种使用姿势 您所在的位置:网站首页 英语翻译boot Spring Validation参数效验各种使用姿势

Spring Validation参数效验各种使用姿势

2023-11-17 00:03| 来源: 网络整理| 查看: 265

日积月累,水滴石穿 😄前言

在日常的项目开发中,为了防止非法参数对业务造成的影响,需要对接口的参数做合法性校验,例如在创建用户时,需要效验用户的账号名称不能输入中文与特殊字符,手机号、邮箱格式是否准确。按照原始的处理逻辑需要对每个接口中的参数进行 if/else 处理,如果这样开发,后期代码难以维护,可读性极差。

为了解决上述问题,validation框架诞生了,代码量大大减少,参数的效验不再穿插业务逻辑代码中,代码美观又易于维护。

基本概念

@Valid 是 JSR303 声明的,JSR是Java Specification Requests的缩写,其中 JSR303 是JAVA EE 6 中的一项子规范,叫做 Bean Validation,为 JavaBean 验证定义了相应的元数据模型和 API,需要注意的是,JSR 只是一项标准,它规定了一些校验注解的规范,但没有实现,而 Hibernate validation 对其进行实现。

Spring Validation 验证框架对参数的验证机制提供了@Validated(Spring JSR-303规范,是标准JSR-303的一个变种)。

@Valid和@Validated区别区别@Valid@Validated来源JSR-303规范Spring是否支持分组不支持支持标注位置METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USETYPE,METHOD,PARAMETER嵌套校验支持不支持基本使用加入依赖 org.springframework.boot spring-boot-starter-web 2.3.12.RELEASE org.projectlombok lombok 1.18.10

注:从 boot-2.3.x开始,spring-boot-starter-web不再引入 spring-boot-starter-validation,所以需要额外手动引入validation依赖,而 2.3之前的版本只需要引入 web 依赖。

org.springframework.boot spring-boot-starter-validation 2.3.12.RELEASE

以上两个依赖都是可以实现功能的。hibernate-validator、spring-boot-starter-validation底层都引入了 jakarta.validation-api依赖。

在实际开发的过程中,请求参数的格式一般有如下几种情况:

对象参数使用

使用对象参数接收分为两种,一种是使用 @RequestBody注解的application/json提交,还有一种不使用 @RequestBody注解的 form-data提交。

使用对象接收参数,在需要校验对象的参数加上 @NotBlank注解,message是校验不通过的提示信息。@Data public class UserReq { @NotBlank(message = "name为必传参数") private String name; @NotBlank(message = "email为必传参数") private String email; }使用 @RequestBodyApi,在需要校验的对象前面加 @RequestBody注解以及@Validated或者@Valid注解,如果校验失败,会抛出MethodArgumentNotValidException异常。@RestController public class GetHeaderController { @PostMapping("save") public void save(@RequestBody @Validated UserReq req){} }不使用 @RequestBody

只需要校验的对象前面加@Validated注解或者@Valid注解,如果校验失败,会抛出BindException异常。

@PostMapping("save2") public void save2(@Validated UserReq req){ }基本类型使用其实也就是路径传参,在参数前面加上相对应的校验注解,还必须在Controller类上加 @Validated注解。如果校验失败,会抛出ConstraintViolationException异常。@RestController @Validated public class GetHeaderController { @PostMapping("get") public void get(@NotBlank(message = "名称 is required") String name,@NotBlank(message = "邮箱 is required") String email) throws JsonProcessingException { } }测试save方法测试

image-20220303181959229.png

save2

image-20220303181652805.png

get方法测试

image-20220303181917983.png

全局异常处理

通过前面的测试,我们知道如果参数校验失败,三种使用场景会抛出三种异常或者警告,分别是MethodArgumentNotValidException、ConstraintViolationException、BindException异常,每种异常的响应格式又不一致。所以在项目开发中,通常会使用统一异常处理来返回一个统一格式并友好的提示。

@RestControllerAdvice public class GlobalExceptionHandler { /** * @RequestBody 上校验失败后抛出的异常是 MethodArgumentNotValidException 异常。 */ @ExceptionHandler(MethodArgumentNotValidException.class) public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); String messages = bindingResult.getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining(";")); return messages; } /** * 不加 @RequestBody注解,校验失败抛出的则是 BindException */ @ExceptionHandler(value = BindException.class) public String exceptionHandler(BindException e){ String messages = e.getBindingResult().getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining(";")); return messages; } /** * @RequestParam 上校验失败后抛出的异常是 ConstraintViolationException */ @ExceptionHandler({ConstraintViolationException.class}) public String methodArgumentNotValid(ConstraintViolationException exception) { String message = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); return message; } }save方法测试

image-20220303182754372.png

save2方法测试

image-20220303182812083.png

get方法测试

image-20220303182841018.png可以发现它是将类中所有的属性进行效验完成之后,才抛出异常的,但其实这有点消耗性能,那能不能只要检测到一个效验不通过的,就抛出异常呢?只需要在容器提供如下代码:

@Configuration public class ParamValidatorConfig { @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() //failFast:只要出现校验失败的情况,就立即结束校验,不再进行后续的校验。 .failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); } @Bean public MethodValidationPostProcessor methodValidationPostProcessor() { MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor(); methodValidationPostProcessor.setValidator(validator()); return methodValidationPostProcessor; } }

MethodValidationPostProcessor是Spring提供的来实现基于方法Method的JSR校验的核心处理器,最终会由 MethodValidationInterceptor进行校验拦截。

测试如下:

image-20220303183616523.png

其余类型

上面举例使用了NotBlank注解,但肯定不只一个!我们进入到 @NotBlank注解所在的包路径。image-20220128151637507.png

哦豁,这么多呀!小杰一个一个来介绍一下作用。

注解备注适用类型示例@AssertFalse被注释的元素必须为 false,null 值是有效的。boolean 和 Boolean@AssertFalse(message = "该参数必须为 false")@AssertTrue被注释的元素必须为 true,null 值是有效的。boolean 和 Boolean@AssertTrue(message = "该参数必须为 true")@DecimalMax被注释的元素必须是一个数字,其值必须小于或等于指定的最大值,null 值是有效的。BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型@DecimalMax(value = "100",message = "该参数不能大于 100")@DecimalMin被注释的元素必须是一个数字,其值必须大于或等于指定的最小值,null 值是有效的。BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型@DecimalMax(value = "0",message = "该参数不能小于 0")@Digits被注释的元素必须是可接受范围内的数字,null 值是有效的。BigDecimal、BigInteger、CharSequence、byte、short、int、long以及包装类型@Digits(integer = 3,fraction = 2,message = "该参数整数位数不能超出3位,小数位数不能超过2位")@Max被注释的元素必须是一个数字,其值必须小于或等于指定的最大值,null 值是有效BigDecimal、BigInteger、byte、short、int、long以及包装类型@Max(value = 200,message = "最大金额不能超过 200")@Min被注释的元素必须是一个数字,其值必须大于或等于指定的最小值,null 值是有效的。BigDecimal、BigInteger、byte、short、int、long以及包装类型@Min(value = 0,message = "最小金额不能小于 0")@Negative被注释的元素必须是负数,null 值是有效BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型@Negative(message = "必须是负数")@NegativeOrZero被注释的元素必须是负数或 0,null 值是有效的。BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型@NegativeOrZero(message = "必须是负数或者为0")@Positive被注释的元素必须是正数,null 值是有效的。BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型@Positive(message = "必须是正数")@PositiveOrZero被注释的元素必须是正数或0,null 值是有效的。BigDecimal、BigInteger、byte、short、int、long、float、double 以及包装类型@PositiveOrZero(message = "必须是正数或者为0")@Future被注释的元素必须是未来的日期(年月日),null 值是有效的。基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant@Future(message = "预约日期要大于当前日期")@FutureOrPresent被注释的元素必须是现在或者未来的日期(年月日),null 值是有效的。基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant@FutureOrPresent(message = "预约日要大于当前日期")@Past被注释的元素必须是过去的日期,null 值是有效的。基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant@Past(message = "出生日期要小于当前日期")@PastOrPresent被注释的元素必须是过去或者现在的日期,null 值是有效的。基本所有的时间类型都支持。常用的:Date、LocalDate、LocalDateTime、LocalTime、Instant@PastOrPresent(message = "出生时间要小于当前时间")@NotBlank被注释的元素不能为空,并且必须至少包含一个非空白字符CharSequence@NotBlank(message = "name为必传参数")@NotEmpty被注释的元素不能为 null 也不能为空CharSequence、Collection、Map、Array@NotEmpty(message = "不能为null或者为空")@NotNull被注释的元素不能为null任意类型@NotNull(message = "不能为null")@Null被注释的元素必须为null任意类型@Null(message = "必须为null")@Email被注释的元素必须是格式正确的电子邮件地址,null 值是有效的。CharSequence@Email(message = "email格式错误,请重新填写")@Pattern被注释的元素必须匹配指定的正则表达式,null 值是有效的。CharSequence@Pattern(regexp = "^1[3456789]\d{9}$",message = "手机号格式不正确")@Size被注释的元素大小必须在指定范围内,null 值是有效的。CharSequence、Collection、Map、Array@Size(min = 5,max = 20,message = "字符长度在 5 -20 之间")

以上注解有几个需要注意一下,因为经常用到,也经常使用错误

@NotNull:适用于任何类型,不能为null,但可以是 (""," ")@NotBlank:只能用于 String,不能为null,而且调用 trim() 后,长度必须大于0,必须要有实际字符。@NotEmpty:用于 String、Collection、Map、Array,不能为null,长度必须大于0。使用分组效验

有些小伙伴说使用 @Validated校验的对象不能复用,这我只能说学的还不够深入。

小杰使用 PayReq来举例,该对象是一个公用的请求体,对接了微信、支付宝两个渠道方,对接微信 payName参数是非必传的,对接支付宝是必传参数。payAmount是两个渠道必传参数。其实就跟平常写新增方法、修改方法一样的,用的是同一个 ReqDTO,但是其中 id 字段新增是不用传递的,而修改时是必传的。

## 定义 ZfbPayGroup

定义ZfbPayGroup的分组接口,继承 Default接口。

public interface ZfbPayGroup extends Default { }

## 添加group

在需要区分组的字段上加 groups 参数。在本例中在 payName加了groups 参数,值为 ZfbPayGroup.class,代表对组为 ZfbPayGroup的进行payName参数校验。

@Data public class PayReq { @NotBlank(message = "支付名称不能为空",groups = {ZfbPayGroup.class}) private String payName; @NotNull(message = "支付金额不能为空") private BigDecimal payAmount; }

注意:ZfbPayGroup 要继承 Default接口,不然 payAmount字段的效验会对ZfbPayGroup这个组失效,payAmount默认的组为 Default。

## 使用 group

创建两个接口,在 zfbPaySave接口中声明@Validated校验组,wxbPaySave接口正常编写。

@PostMapping("zfbPaySave") public void zfbPaySave(@RequestBody @Validated(value = {ZfbPayGroup.class}) PayReq req) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); System.out.println( mapper.writeValueAsString(req)); } @PostMapping("wxbPaySave") public void wxbPaySave(@RequestBody @Validated PayReq req) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); System.out.println( mapper.writeValueAsString(req)); }

## 测试

zfbPaySave:提示支付名称不能为空

image-20220215163650039.png

wxbPaySave:没被校验拦截。

image-20220215163732369.png

嵌套校验

什么是嵌套使用呢?就是一个对象中包含另外一个对象,另外一个对象的字段也是需要进行校验。示例如下:

UserReq@Data public class UserReq { @NotBlank(message = "name为必传参数") private String name; private String email; @NotNull(message = "proReq对象不能为空") @Valid private ProReq proReq; }

嵌套校验需要在效验的对象加上 @Valid 注解。

ProReq@Data public class ProReq { @NotBlank(message = "proName为必传参数") private String proName; }测试

image-20220215153918592.png

集合校验

在某些场景下,我们需要使用集合接收前端传递的参数,并对集合中的每个对象都进行参数校验。但是这时我们的参数校验并不会生效!如下写法:

@PostMapping("save3") public String save3(@RequestBody @Validated List req){ return "成功"; }

image-20220304140721319.png

下面介绍两种方式对集合进行效验!

方式一

@Validated + @Valid两个注解同时使用!缺点:不能使用分组效验!如果该实体不需要用到分组功能,可以使用该方式!

@RestController @Validated public class GetHeaderController { @PostMapping("save3") public String save3(@RequestBody @Valid @NotEmpty(message = "该集合不能为空") List req){ return "成功"; } }测试

image-20220304145951412.png

方式二自定义一个List@Data public class ValidList implements List { // 使用该注解就不需要手动重新 List 中的方法了 @Delegate @Valid public List list = new ArrayList(); }@Delegate,为 lombok 的注解,表示该属性的所有对象的实例方法都将被该类代理。编码如下:@PostMapping("save4") public String save4(@RequestBody @Validated @NotEmpty(message = "该集合不能为空") ValidList req){ return "成功"; }测试

image-20220304150714648.png

自定义校验规则

自定义校验规则小杰在工作当中用的比较少。大部分业务需求使用自带的注解已经够平常开发了。当然自定义validation规则也非常简单。这里使用校验电话号码是否合法来举例!别抬杠,说我为什么不用 @Pattern(regexp = "^1[3456789]\\d{9}$",message = "手机号格式不正确")直接实现。对不起,我不想每次写正则。

自定义注解 Phone,跟着内置的注解照葫芦画瓢。不过 validatedBy的值要指定我们自定义的约束验证器!@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = { PhoneValidator.class }) public @interface Phone { String message() default "手机号码格式异常"; Class[] groups() default { }; Class


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有