《JSR303参数校验》

一、基础概述

1.简介

Java API 规范 (JSR303) 定义了 Bean 校验的标准 validation-api,但没有提供实现。hibernate validation 是对这个规范的实现,并增加了校验注解如 @Email、@Length 等。Spring Validation 是对 hibernate validation 的二次封装,用于支持 spring mvc 参数自动校验。

2.依赖

<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 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

POSTPUT 请求,使用@RequestBody传递参数
GET 请求,使用 @RequestParam / @PathVariable 传递参数

1.@RequestParam参数校验

@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即正常

2.@PathVariable参数效验

@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;}}

3.@RequestBody参数效验

在实体类上生命效验字段

package com.h3c.entity;import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;@Data
public class UserParam {@NotNullprivate 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

4.全局异常处理

前面说过,如果校验失败,会抛出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检查该字段必须为TrueBoolean
@AssertFalse检查该字段必须为FalseBoolean
@Min(value)被注解的字段必须大于等于指定的最小值
@Max(value)被注解的字段必须小于等于指定的最大值
@Negative带注解的元素必须是严格的负数(0被认为是无效值)BigDecimal,BigInteger,byte,short,int,long及其包装类
@NegativeOrZero带注解的元素必须是严格的负数或0BigDecimal,BigInteger,byte,short,int,long及其包装类
@Positive带注解的元素必须是严格的正数(0被认为是无效值)BigDecimal,BigInteger,byte,short,int,long及其包装类
@PositiveOrZero带注解的元素必须是严格的正数或0BigDecimal,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被注解的字段必须是现在的或将来的日期时间
@Email字符串必须是格式正确的电子邮件地址String
@Pattern(value)被注解的字段必须符合指定的正则表达式

四、高级使用

1.分组校验

在实际项目中,可能多个接口需要使用同一个 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;@NotNullprivate 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;
}

2.嵌套校验

当我们实体类中某个字段是对象,这种情况下,可以使用嵌套校验

@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})@Validprivate Job job;// 可以针对集合对象参数校验@NotNull(groups = {UpdateValidGroup.class, InsertValidGroup.class})@Validprivate List<Job> jobs;@Datapublic 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;}}

【接口情况】
在这里插入图片描述

3.集合校验

【参数类】

@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;}}

【测试】
在这里插入图片描述

4.自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。

例如性别参数只能是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");@Overridepublic 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@CheckValidprivate 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;}}

【测试】
在这里插入图片描述

5.编程式校验

上面的示例都是基于注解来实现自动校验的,在某些情况下,我们可能希望以编程方式调用验证。这个时候可以注入 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;
}

6.快速失败

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 {@Beanpublic Validator validator() {ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class).configure()// 快速失败模式.failFast(true).buildValidatorFactory();return validatorFactory.getValidator();}}

【测试】
在这里插入图片描述

7.@Valid 和 @Validated 区别

image-20210525021620127

五、源码分析

1.@RequestBody参数校验实现原理

a>RequestResponseBodyMethodProcessor

Spring-MVC框架中,RequestResponseBodyMethodProcessor是用于解析 @RequestBody标注的参数以及处理 @ResponseBody 标注方法的返回值的。显然,执行参数校验的逻辑肯定就在解析参数的方法 resolveArgument() 中:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {@Overridepublic 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);}
}

b>validateIfApplicable()

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {// 获取参数前面设置的注解,注解中肯定会包含@RequestBodyAnnotation[] annotations = parameter.getParameterAnnotations();for (Annotation ann : annotations) {// 判断是否存在@Validated,通过一个工具类来实现Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);if (validationHints != null) {binder.validate(validationHints);break;}}
}

在这里插入图片描述

c>determineValidationHints()

需要声明的是,在接口参数前面增加校验注解,注解可以为@Validated或者@Valid,2个都可以

@Valid注解判断存在即可通过判断,而@Validated注解判断存在后,还需要尝试获取里面的分组校验

public abstract class ValidationAnnotationUtils {private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];@Nullablepublic 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});}}

d>validate()

上面验证了参数是否标注了@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);}}
}

2.@RequestParam参数校验实现原理


{// 获取到参数对象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);}}
}

2.@RequestParam参数校验实现原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/318738.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux centos 部署 jenkins

一、试了用容器部署&#xff0c;不行。(建议别用容器部署) 二、直接部署在主机上 1、安装java环境 yum install java-11-openjdk-devel # 检查 java -version # 打印 [rootiZwz9a99mctbkabkh2imhdZ init.d] java -version openjdk version "11.0.21" 2023-10-17 L…

Django Web 开发实战-实现用户管理系统(部门管理、用户管理、注册登录、文件上传)

简介 基于Django Python Web框架 MySQL Bootstrap 开发的用户管理系统。支持增删改查、模糊搜索、分页。 功能介绍 部门管理---》已完成 用户管理---》已完成 认证&#xff08;注册/登录&#xff09;---》开发中 数据统计---》待开发 文件上传---》待开发 效果图 部门…

设计模式之建造者模式【创造者模式】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档> 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。各位小伙伴&#xff0c;如果您&#xff1a; 想系统/深入学习某…

1.3 力扣二叉树中等题

题目一&#xff1a; 669. 修剪二叉搜索树 给你二叉搜索树的根节点 root &#xff0c;同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即&#xff0c;如果没有被移除&…

Java中的异常如何处理?

在Java编程中&#xff0c;异常是一种在程序执行期间发生的意外情况。学会处理异常是编写健壮、可靠程序的关键之一。 为什么需要异常处理&#xff1f; 异常处理允许程序在发生错误时进行优雅而有序的处理&#xff0c;而不是导致程序崩溃。这有助于提高程序的可维护性和用户体…

【办公软件】修改U盘的默认盘符

在工作中我们可能会因为有一些大型软件设置了库文件路径&#xff08;如Z盘&#xff09;。在家办公时通过U盘的方式将库拷入在U盘中&#xff0c;但是到家里的电脑上&#xff0c;U盘插入后会默认一个盘符&#xff08;如E盘&#xff09;&#xff0c;那么应该怎么操作呢&#xff1f…

UE5 产品三维交互展示 创意收集

1. 无人机展示 https://www.bilibili.com/video/BV12N4y1g7gA/?spm_id_from333.337.search-card.all.click&vd_source707ec8983cc32e6e065d5496a7f79ee6 开场是场景漫游重要部件靠近观察颜色调整拆分与合并&#xff0c;过程流畅拆分后靠近观察产品动画&#xff0c;动画中&…

音效出众设计时尚,内置AI功能,sanag塞那Z50上手

现在蓝牙耳机已经成为人们生活中不可或缺的一部分了&#xff0c;像是在上班、坐车的时候&#xff0c;既可以享受自己的音乐空间&#xff0c;又不会吵到别人&#xff0c;看书、做题还是运动的时候&#xff0c;也可以保证长时间使用耳朵卫生、舒适度。正因为庞大的市场需求&#…

深圳找工作一般去哪里找

深圳找工作一般在 吉鹿力招聘网上找 吉鹿力招聘网是一个权威的招聘平台&#xff0c;基本可以信任。公司通常先通过吉鹿力招聘网发布招聘信息。而求职者也可以先在吉鹿力招聘网网上了解招聘信息&#xff0c;然后投递简历。因为吉鹿力招聘网是一个综合性、专业性较强的地方&…

机器人技能学习--数据集剖析

文章目录 前言数据总览数据介绍actionsrobot0_eef_poserobot0_eef_quatstatesobject 参考资料 前言 一切为了能自己构建属于自己的数据集&#xff0c;所以&#xff0c;从现有数据集剖析入手。    目前&#xff0c;基于 MimicGen 官方提供的数据集&#xff0c;初始数据集有11组…

【linux】ufw 的基本使用

碎碎念 所有的云平台的网络流量的进出基本上有三层&#xff0c;首先是虚拟网的流量控制&#xff0c;一般是通过子网访问控制列表来控制vpc也好子网也好的流量出入&#xff0c;其次是安全组控制一层&#xff0c;通过安全组规则控制一类/一组主机&#xff08;指EC2/ECS/VM/CE这些…

看完,你还会学鸿蒙吗?

是不是前端程序员的春天&#xff0c;我们可以分析鸿蒙现在的市场和布局。其实不仅仅只是前端&#xff0c;还有Android、Java、Python等等开发人员都可以把鸿蒙当做新的出路。 2024年程序员为什么一定要学鸿蒙&#xff1f; 首先&#xff0c;鸿蒙作为一个新系统的出现。它的结构…