由浅入深使用validation框架进行参数校验

news/2023/6/7 23:57:08

1 引言

平时在业务开发过程中 controller 层的参数校验有时会存在下面这样的判断

public String add(UserVO userVO) {if(userVO.getAge() == null){return "年龄不能为空";}if(userVO.getAge() > 120){return "年龄不能超过120";}if(userVO.getName().isEmpty()){return "用户名不能为空";}// 省略一堆参数校验...return "OK";
}

业务代码还没开始写呢,光参数校验就写了一堆判断。这样写虽然没什么错,但是给人的感觉就是:不优雅,不专业。

其实java给我们提供了一组规范:JSR-303,spring又实现并扩展了这个规范,让我们能够优雅无侵入的实现参数的校验

2 JSR-303 规范

2.1 JSR:Java 规范提案

JSR即为Java Specification Requests的缩写,是Java 规范提案,常常定义一组规则,有的规则有默认的实现,有的则需要用户自己去实现。

2.2 JSR-303:Bean Validation规范

JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation,为Bean验证定义了元数据模型和API,默认的元数据模型是通过Annotations注解来描述的,支持扩展。

官方参考的默认实现是Hibernate Validator,当然用户也可以自己扩展,比如常用的Spring JSR-303

注意:这只是一组规范,对应到代码就是一组接口,不能直接使用
这组规范在javax所提供的jar:jakarta.validation-api-xxx.jar中,要想使用,则需要导入并实现这组规范

 <dependency><groupId>jakarta.validation</groupId><artifactId>jakarta.validation-api</artifactId><version>2.0.2</version></dependency>

2.3 JSR-303 基本的约束原理

一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。

也就是说可以有多个 constraint validator 对应一个 annotation。

在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。

  • constraint:一个约束
    • annotation:注解
    • constraint validator:约束校验器(可有多个)

有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制,可以通过两种方法去实现。

  • 一种是组合现有的 constraint 来生成一个更复杂的 constraint
  • 另外一种是开发一个全新的 constraint。

2.4 JSR-303 中内置的约束

内置的约束注解如下图:
在这里插入图片描述

3 JSR-303规范的实现

3.1 Hibernate Validator

这是官方参考的默认实现,Hibernate JSR-303导入并实现了jakarta.validation-api,并对JSR-303默认的约束做了扩展,注意此实现与 Hibernate ORM 没有任何关系。

3.1.1 Hibernate Validator 附加的 constraint

Constraint详细信息
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

3.1.2 导入依赖

<dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-el</artifactId><version>9.0.63</version><scope>compile</scope>
</dependency>
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.2.3.Final</version><scope>compile</scope>
</dependency>

3.1.3 简单使用

package myValid;import lombok.Data;import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.util.Iterator;
import java.util.Set;@Data
public class User {@NotNull(message = "email不可以为空")@Email(message = "email不合法")private String email;public static void main(String[] args) {final User user = new User();user.setEmail("xxxxxx.com");//调用JSR303验证工具,校验参数Validator validator = Validation.buildDefaultValidatorFactory().getValidator();Set<ConstraintViolation<User>> violations = validator.validate(user);Iterator<ConstraintViolation<User>> iter = violations.iterator();if (iter.hasNext()) {final ConstraintViolation<User> violation = iter.next();// email:email不合法System.out.println(violation.getPropertyPath() + ":" + violation.getMessage());// TODO 可以进行指定约束异常的抛出}}}

3.2 Spring JSR-303

3.2.1 官方说明

JSR-303的javax.validation的变体。有效,支持验证组的规范。为方便使用Spring的JSR-303支持而设计,但不是JSR-303特有的。

可以与Spring MVC处理程序方法参数一起使用。通过org.springframework.validation.SmartValidator的验证提示概念支持,验证组类充当提示对象。

也可以与方法级验证一起使用,表示特定类应该在方法级进行验证(充当相应验证拦截器的切入点),但也可以在带注释的类中为方法级验证指定验证组。在方法级别应用此注释允许覆盖特定方法的验证组,但不作为切入点;然而,类级注释对于触发特定bean的方法验证是必要的。也可以用作自定义原型注释或自定义组特定的验证注释上的元注释。

3.2.2 通俗解释

  • 实现了JSR-303标准
  • 支持分组验证(特有)
  • 可以与Spring MVC收参的方法一起使用

4 Spring Validator的使用

4.1 导入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

点进入发现Spring Validator包含了Hibernate Validator,所以,spring环境下,我们直接使用Spring Validator会非常非常方便

  <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.7.0</version><scope>compile</scope></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-el</artifactId><version>9.0.63</version><scope>compile</scope></dependency><dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId><version>6.2.3.Final</version><scope>compile</scope></dependency></dependencies>

4.2 常用的约束说明

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,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(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、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注解即可级联验证

4.3 重点:使用技巧

一般情况下,我们都是校验方法上面的形参,既然是形参,就分两种情况:

  • 一种是校验形参本身,这种情况需要在当前类上面添加@Validated注解,同时使用@NotNull等注解对该形参进行校验
  • 另一种则是形参内部属性的校验,这种情况可以在当前类上面添加@Validated注解,也可以不加,但是一定要在该形参上添加@Validated注解
  • 未通过校验时会抛出org.springframework.web.bind.MethodArgumentNotValidExceptionorg.springframework.validation.BindException等异常,我们捕获统一返回即可

下面进行举例说明,比如说我们有一个DocumentTransController

@RestController
@RequestMapping("/documentTrans")
public class DocumentTransController {private final DocumentTransService documentTransService;public DocumentTransController(DocumentTransService documentTransService) {this.documentTransService = documentTransService;}/*** 接收翻译请求(接收文件)* @return*/@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)public String transDocumentByFile(MultipartFile file,DocumentInfoBo documentInfoBo) {return documentTransService.documentTrans(file,documentInfoBo);}}@Data
public class DocumentInfoBo {/*** 所属用户id*/@NotNull(message = "用户id不能为空")private Long userId;private UserInfo userInfo;@Datastatic class UserInfo {@NotNull(message = "用户名不能为空")private String username;private String phone;}
}    

4.3.1 校验方法形参本身

我现在想要校验transDocumentByFile()的file不能为空,则需要在DocumentTransController 类上添加@Validated注解,同时在(MultipartFile file)前面添加@NotNull 注解,如下:

@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {private final DocumentTransService documentTransService;public DocumentTransController(DocumentTransService documentTransService) {this.documentTransService = documentTransService;}/*** 接收翻译请求(接收文件)* @return*/@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)public String transDocumentByFile(@NotNull MultipartFile file, DocumentInfoBo documentInfoBo) {return documentTransService.documentTrans(file,documentInfoBo);}}

4.3.2 校验方法形参内部属性

在4.3.1的基础上,我现在想要校验DocumentInfoBo对象内的userId属性不能为空,则需要在(DocumentInfoBo documentInfoBo)前面添加@Validated注解

@RestController
@RequestMapping("/documentTrans")
@Validated
public class DocumentTransController {private final DocumentTransService documentTransService;public DocumentTransController(DocumentTransService documentTransService) {this.documentTransService = documentTransService;}/*** 接收翻译请求(接收文件)* @return*/@RequestMapping(value = "/documentTransByFile", method = RequestMethod.POST)public String transDocumentByFile(@NotNull MultipartFile file,@Validated DocumentInfoBo documentInfoBo) {return documentTransService.documentTrans(file,documentInfoBo);}}

4.3.3 嵌套验证

现在我想在4.3.1和4.3.2基础上,添加对userInfo属性的校验,则需要在userInfo上面添加 @Valid @NotNull(message = "userInfo不能为空")即可

@Data
public class DocumentInfoBo {/*** 所属用户id*/@NotNull(message = "用户id不能为空")private Long userId;@Valid@NotNull(message = "userInfo不能为空")private UserInfo userInfo;@Datastatic class UserInfo {@NotNull(message = "用户名不能为空")private String username;private String phone;}
}    

4.3.4 分组参数校验

第一步、定义一个校验组类,声明四个接口对应不同场景校验

public class ValidationGroups {public interface Select {}public interface Insert {}public interface Update {}public interface Detail {}public interface Delete{}
}

第二步、在实体类具体属性添加校验规则及校验分组

@Data
public class Person {@NotNull(message = "personId不能为null",groups = Select.class)private Integer personId;@NotEmpty(message = "name不能为空",groups = Insert.class)private String name;private Integer age;
}

第三步、在控制层创建查询和新增接口,添加@Validated注解,指定分组,可多个。

    @GetMapping("/select")public Object select(@Validated({ValidationGroups.Select.class}) Person person) {return person;}@GetMapping("/insert")public Object insert(@Validated(ValidationGroups.Insert.class) Person person) {return person;}

4.4 重点:注意事项

Spring Validator不光可以用在Controller层,同样也可以作用于service、或者其他bean

4.5 重点:@Validated和@Valid的区别

很多时候@Validated和@Valid的界限分的并没有那么清楚,很多场景@Validated和@Valid效果也等同,那么他们到底有什么区别呢?什么时候该用哪个注解呢?

在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:

@Validated@Valid
分组提供分组功能,可在入参验证时,根据不同的分组采用不同的验证机制。无分组功能
可注解位置可以用在类型、方法和方法参数上。但是不能用在成员属性上可以用在方法、构造函数、方法参数和成员属性上(两者是否能用于成员属性上直接影响能否提供嵌套验证的功能)
嵌套验证用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性上。也无法提供框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。
  • @Valid 被注释的元素是一个对象,需要检查此对象的所有字段值
  • @Validated 被注解的元素是一个对象或者一个类,需要检查此对象的所有字段值

使用建议:常规情况使用@Validated注解,嵌套验证时使用@Valid注解

4.6 校验异常统一处理示例

异常对应:

  • 使用form data方式调用接口,校验异常抛出 BindException
  • 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
  • 单个参数校验异常抛出ConstraintViolationException
@RestControllerAdvice
public class ExceptionHandle {private final Logger logger = LoggerFactory.getLogger(getClass());@ExceptionHandler(value = Exception.class)public JSONResult<Object> handle(Exception e) {logger.error(e.getMessage(), e);if (e instanceof MyException) {MyException myException = (MyException) e;return JSONResult.error(myException.getCode(),myException.getMessage());} else {return JSONResult.error(ResultEnum.UNKNOWN_ERROR.getCode(), e.getMessage());}}/*** 方法参数校验* 由于spring捕获异常的问题,导致此异常的编码格式无法修改为UTF-8,* 故不使用RestControlle的ResponseBody,自己实现httpServletResponse的IO。*/@ExceptionHandler(MethodArgumentNotValidException.class)public JSONResult<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {final BindingResult bindingResult = e.getBindingResult();List<FieldError> fieldErrors = bindingResult.getFieldErrors();String msg = ResultEnum.VALIDATION_ERROR.getMsg();if(!fieldErrors.isEmpty()){// 返回错误信息msg = fieldErrors.get(0).getField()+":"+fieldErrors.get(0).getDefaultMessage();}logger.error(e.getMessage(), e);return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);}/*** 捕获校验异常* @param e* @return*/@ExceptionHandler(ValidationException.class)public JSONResult<Object> handleValidationException(ValidationException e) {logger.error(e.getMessage(), e);return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getCause().getMessage());}/*** 捕获校验绑定异常* @param e* @return*/@ExceptionHandler(BindException.class)public JSONResult<Object> handleBindException(BindException e) {logger.error(e.getMessage(), e);List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();String msg = "";if(!fieldErrors.isEmpty()){// 返回错误信息msg = fieldErrors.get(0).getDefaultMessage();}return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), msg.isEmpty()?e.getCause().getMessage():msg);}/*** 捕获违反约束异常* @param e* @return*/@ExceptionHandler(ConstraintViolationException.class)public JSONResult<Object> handConstraintViolationException(ConstraintViolationException e) {final Iterator<ConstraintViolation<?>> iterator = e.getConstraintViolations().iterator();while (iterator.hasNext()) {final ConstraintViolation<?> constraintViolation = iterator.next();final String errorMsg = constraintViolation.getMessage();logger.error(e.getMessage(), e);return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), errorMsg);}return JSONResult.error(ResultEnum.VALIDATION_ERROR.getCode(), e.getMessage());}

5 扩展:注解失效的原因排查

1、是否忘记导入依赖
在2.3.0版本之前spring-boot-starter-web是默认集成validation依赖的,但是在2.3.0开始就去掉了该依赖,所以需要自己添加。

2、如果要校验对象本身,则需要在所在类上面添加@Validated注解
在这里插入图片描述
3、涉及嵌套验证,是否忘记添加@Valid注解

6 使用ConstraintValidator自定义注解和校验器

https://blog.csdn.net/weixin_43702146/article/details/125657418

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

如若内容造成侵权/违法违规/事实不符,请联系郑州代理记账网进行投诉反馈,一经查实,立即删除!

相关文章

微信公众平台开发——图文消息加表情

本篇博客参考与柳峰老师的博客&#xff1a;https://blog.csdn.net/lyq8479/article/details/9393097 1、最近开发微信公众号&#xff0c;用户要求图文消息中要有一个小星星的图标&#xff0c;这让我很费解啊。。但当我看到柳峰老师的博客时&#xff0c;宛如山重水复疑无路&…

微信不同平台登录邮箱不同怎么破

今天注册微信开发平台&#xff0c;输入qq邮箱&#xff0c;提示该邮箱已被占用&#xff0c;之前使用过这个邮箱注册了微信公众平台。 如果你没有这么多邮箱&#xff0c;可以通过qq邮箱设置多个邮箱账号。这样就可以顺利注册了。 「更多精彩内容请关注公众号geekymv&#xff0c…

计算机主机内部配件有哪些,[计算机维护常识]主机内部有哪些附件

什么是电脑配件&#xff1f;今天&#xff0c;编辑器会将有关计算机硬件组成的基本知识推广给许多计算机新手. 许多新手朋友都不知道计算机主机由哪些配件组成. 在Internet上搜索的信息不清楚或格式太长&#xff0c;因此我认为有必要以简洁直观的方式解释计算机附件的组成.不用说…

学生晚上回宿舍时其在实验室的计算机主机,学生晚上回宿舍时,其在实验室的计算机主机应关闭,显示器一般不用关闭。...

学生显示法的本质是什么&#xff1f;()权利仅存在于法律领域中&#xff0c;晚上其他领域中不存在权利。()法学所研究的法是一个动态的概念&#xff0c;回宿不仅指纸面上的法&#xff0c;而且还指现实生活中的法。()舍时实验室哈罗德-多马模型中假设()。其器哈罗德-多马模型自然…

解决计算机主机 缓冲区,为解决计算机主机与打印机间速度不匹配问题,通常设一个打印数据缓冲区。主机将要输出的数据依次写入该缓冲区,而打印机则依次从该缓冲区中取出数据。该缓冲区的逻辑结构应该是( )。..

为解问题2&#xff0e; 两种血浆脂蛋白分类方法是_____________和____________。决计间速机将机则辑结下列字符数组长度为5的是()算机数据数据数据下列数据中属于字符串常量的是( )主机判断两个字符串是否相等,正确的表达方式是( )打印机个要输依次依次以下正确的字符串常量是(…

计算机主机由cpu和内储存器构成,计算机主机由CPU、存储器和硬盘组成。

摘要&#xff1a;小儿心开国家推拿中医展了作近年药管一系为中理局来以列工发展&#xff0c;计算机主机由包括。见字就可以出声&#xff0c;和硬需要感情加入朗读时不。学化学教学中向化学师、盘组学学(化学在中教育技术)教用面育硕的应范生士科教&#xff0c;化学广大中学教师…

比YOLOv8还要强的YOLOv6 v3.0

论文地址&#xff1a;https://arxiv.org/pdf/2301.05586.pdf 开源地址&#xff1a;https://github.com/meituan/YOLOv6 YOLOv6 v3.0的主要贡献简述如下&#xff1a; 对检测器的Neck部件进行了翻新&#xff0c;引入BiC(Bi-directional Concatenation)提供更精确的定位信息&…

献给迷失的你—一名IT员工的职场心得

http://elevencitys.com/2013/11/%E7%8C%AE%E7%BB%99%E8%BF%B7%E5%A4%B1%E7%9A%84%E4%BD%A0-%E4%B8%80%E5%90%8Dit%E5%91%98%E5%B7%A5%E7%9A%84%E8%81%8C%E5%9C%BA%E5%BF%83%E5%BE%97/ 这些日子我一直在写一个实时操作系统内核&#xff0c;已有小成了&#xff0c;等写完我会全部…