1. 前言 因为网络传输的不可靠性,以及前端数据控制的可篡改性,后端的参数校验是必须的,应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。
基于 spring boot + mybatis plus + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/ruoyi-vue-pro 视频教程:https://doc.iocoder.cn/video/ 2. 数据校验的痛点 为了保证数据语义的正确,我们需要进行大量的判断来处理验证逻辑。而且项目的分层也会造成一些重复的校验,产生大量与业务无关的代码。不利于代码的维护,增加了开发人员的工作量。
基于 spring cloud alibaba + gateway + nacos + rocketmq + vue & element 实现的后台管理系统 + 用户小程序,支持 rbac 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/yunaiv/yudao-cloud 视频教程:https://doc.iocoder.cn/video/ 3. jsr 303 校验规范及其实现 为了解决上面的痛点,将验证逻辑与相应的领域模型进行绑定是十分有必要的。为此产生了jsr 303 – bean validation 规范。hibernate validator 是jsr-303 的参考实现,它提供了jsr 303 规范中所有的约束(constraint)的实现,同时也增加了一些扩展。
hibernate validator 提供的常用约束注解 约束注解 详细信息
@null 被注释的元素必须为 null
@notnull 被注释的元素必须不为 null
@asserttrue 被注释的元素必须为 true
@assertfalse 被注释的元素必须为 false
@min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@decimalmin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@decimalmax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@size(max, min) 被注释的元素的大小必须在指定的范围内
@digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@past 被注释的元素必须是一个过去的日期
@future 被注释的元素必须是一个将来的日期
@pattern(value) 被注释的元素必须符合指定的正则表达式
@email 被注释的元素必须是电子邮箱地址
@length 被注释的字符串的大小必须在指定的范围内
@notempty 被注释的字符串的必须非空
@range 被注释的元素必须在合适的范围内
4. 验证注解的使用 在spring boot 开发中使用hibernate validator 是非常容易的,引入下面的starter 就可以了:
org.springframework.boot spring-boot-starter-validation 一种可以实现接口来定制validator,一种是使用约束注解。胖哥觉得注解可以满足绝大部分的需求,所以建议使用注解来进行数据校验。而且注解更加灵活,控制的粒度也更加细。接下来我们来学习如何使用注解进行数据校验。
4.1 约束注解的基本使用 我们对需要校验的方法入参进行注解约束标记,例子如下:
@datapublic class student { @notblank(message = 姓名必须填) private string name; @notnull(message = 年龄必须填写) @range(min = 1,max =50, message = 年龄取值范围1-50) private integer age; @notempty(message = 成绩必填) private list scores;} post 请求 然后定义一个post 请求的spring mvc 接口:
@restcontroller@requestmapping(/student)public class studentcontroller { @postmapping(/add) public rest addstudent(@valid @requestbody student student) { return restbody.okdata(student); }} 通过对addstudent方法入参添加@valid来启用参数校验。当使用下面数据进行请求将会抛出methodargumentnotvalidexception异常,提示age范围超出1-50。
post /student/add http/1.1host: localhost:8888content-type: application/json{ name: felord.cn, age: 77, scores: [ 55 ]} get 请求 如法炮制,我们定义一个get 请求的接口:
@getmapping(/get)public rest getstudent(@valid student student) { return restbody.okdata(student);} 使用下面的请求可以正确对学生分数scores进行了校验,但是抛出的并不是methodargumentnotvalidexception异常,而是bindexception异常。这和使用@requestbody注解有关系,这对我们后面的统一处理非常十分重要。
get /student/get?name=felord.cn&age=12 http/1.1host: localhost:8888 自定义注解 可能有些同学注意到上面的年龄我进行了这样的标记:
@notnull(message = 年龄必须填写)@range(min = 1,max =50, message = 年龄取值范围1-50)private integer age; 这是因为@range不会去校验为空的情况,它只处理非空的时候是否符合范围约束。所以要用多个注解来约束。如果我们某些场景需要重复的捆绑多个注解来使用时,可以使用自定义注解将它们封装起来组合使用,下面这个注解就是将@notnull和@range进行了组合,你可以仿一个出来用用看。
import org.hibernate.validator.constraints.range;import javax.validation.constraint;import javax.validation.payload;import javax.validation.reportassingleviolation;import javax.validation.constraints.notnull;import javax.validation.constraintvalidation.supportedvalidationtarget;import javax.validation.constraintvalidation.validationtarget;import java.lang.annotation.*;/** * @author a * @since 17:31 **/@constraint( validatedby = {})@supportedvalidationtarget({validationtarget.annotated_element})@retention(retentionpolicy.runtime)@target({elementtype.method, elementtype.field, elementtype.annotation_type, elementtype.constructor, elementtype.parameter, elementtype.type_use})@notnull@range(min = 1, max = 50)@documented@reportassingleviolationpublic @interface age { // message 必须有 string message() default 年龄必须填写,且范围为 1-50 ; // 可选 class[] groups() default {}; // 可选 class[] payload() default {};} 还有一种情况,我们在后台定义了枚举值来进行状态的流转,也是需要校验的,比如我们定义了颜色枚举:
public enum colors { red, yellow, blue} 我们希望入参不能超出colors的范围[red, yellow, blue],这就需要实现constraintvalidator接口来定义一个颜色约束了,其中泛型a为自定义的约束注解,泛型t为入参的类型,这里使用字符串,然后我们的实现如下:
/** * @author felord.cn * @since 17:57 **/public class colorconstraintvalidator implements constraintvalidator { private static final set color_constraints = new hashset(); @override public void initialize(color constraintannotation) { colors[] value = constraintannotation.value(); list list = arrays.stream(value) .map(enum::name) .collect(collectors.tolist()); color_constraints.addall(list); } @override public boolean isvalid(string value, constraintvalidatorcontext context) { return color_constraints.contains(value); }} 然后声明对应的约束注解color,需要在元注解@constraint中指明使用上面定义好的处理类colorconstraintvalidator进行校验。
/** * @author felord.cn * @since 17:55 **/@constraint(validatedby = colorconstraintvalidator.class)@documented@target({elementtype.method, elementtype.field, elementtype.annotation_type, elementtype.constructor, elementtype.parameter, elementtype.type_use})@retention(retentionpolicy.runtime)public @interface color { // 错误提示信息 string message() default 颜色不符合规格; class[] groups() default {}; class[] payload() default {}; // 约束的类型 colors[] value();} 然后我们来试一下,先对参数进行约束:
@datapublic class param { @color({colors.blue,colors.yellow}) private string color;} 接口跟上面几个一样,调用下面的接口将抛出bindexception异常:
get /student/color?color=cay http/1.1host: localhost:8888 当我们把参数color赋值为blue或者yellow后,能够成功得到响应。
4.2 常见问题 在实际使用起来我们会遇到一些问题,这里总结了一些常见的问题和处理方式。
检验基础类型不生效的问题 上面为了校验颜色我们声明了一个param对象来包装唯一的字符串参数color,为什么直接使用下面的方式定义呢?
@getmapping(/color)public rest color(@valid @color({colors.blue,colors.yellow}) string color) { return restbody.okdata(color);} 或者使用路径变量:
@getmapping(/rest/{color})public rest rest(@valid @color({colors.blue, colors.yellow}) @pathvariable string color) { return restbody.okdata(color);} 上面两种方式是不会生效的 。不信你可以试一试,起码在spring boot 2.3.1.release 是不会直接生效的。
使以上两种生效的方法是在类上添加@validated注解。注意一定要添加到方法所在的类上才行 。这时候会抛出constraintviolationexception异常。
集合类型参数中的元素不生效的问题 就像下面的写法,方法的参数为集合时,如何检验元素的约束呢?
/** * 集合类型参数元素. * * @param student the student * @return the rest */@postmapping(/batchadd)public rest batchaddstudent(@valid @requestbody list student) { return restbody.okdata(student);} 同样是在类上添加@validated注解。注意一定要添加到方法所在的类上才行 。这时候会抛出constraintviolationexception异常。
嵌套校验不生效 嵌套的结构如何校验呢?打个比方,如果我们在学生类student中添加了其所属的学校信息school并希望对school的属性进行校验。
@datapublic class student { @notblank(message = 姓名必须填) private string name; @age private integer age; @notempty(message = 成绩必填) private list scores; @notnull(message = 学校不能为空) private school school;}@datapublic class school { @notblank(message = 学校名称不能为空) private string name; @min(value = 0,message =校龄大于0 ) private integer age;} 当 get 请求时正常校验了school的属性,但是post 请求却无法对school的属性进行校验。这时我们只需要在该属性上加上@valid注解即可。
@datapublic class student { @notblank(message = 姓名必须填) private string name; @age private integer age; @notempty(message = 成绩必填) private list scores; @valid @notnull(message = 学校不能为空) private school school;} 每加一层嵌套都需要加一层@valid注解。通常在校验对象属性时,@notnull、@notempty和@valid配合才能起到校验效果。
5. 总结 通过校验框架我们可以专心于业务开发,本文对hibernate validator 的使用和一些常见问题进行了梳理。我们可以通过spring boot 统一异常处理来解决参数校验的异常信息的提示问题。
6. 项目源码地址 https://github.com/notfound403/spring-boot-validation
三星S8和iphone7plus哪个好?三星S8怎么截屏?三星S8精美壁纸欣赏
2012年全球半导体厂商研发支出创新高
LM350汽车电池充电器电路
Chiplet会在中国芯片产业出奇效吗
DDR工作原理_DDR DQS信号的处理
JSR 303校验规范及其实现
手机信号放大器如何实现双向放大
微软已调整Office套件支持时间,Office这个版本将被放弃
我国累计进口缝制机械产品出口额再创新高
曝Zen2架构性能将提升16%
普洛帝OPC-III油液综合监测系统在工程机械行业的应用介绍
Python中的默认编码
过去几个月,他们把数字化融进了中国经济的毛细血管
加密货币不言底 离得越远越好
中移物联采购600万台M5330-A车载通信模块,全力着手车联网市场
小米6或于4月11发布,全系骁龙835+陶瓷尊享版+两版本,货量充足不抢购?
开关稳压器的特点 与线性稳压器的区别
巨头开启人工智能时代,改变人类未来的变革正发生!
平板示波器如何进行探头的补偿和衰减系数设定
人工智能医药研发领域存在哪三种商业模式