springboot 如何进行参数校验在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,例如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。靠代码对接口参数一个个校验的话就太繁琐了,代码可读性极差。
validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验,用户名必须位于6到12之间 等等…
validator校验框架遵循了jsr-303验证规范(参数校验规范), jsr是java specification requests的缩写。
1.集成validator校验框架1.1. 引入依赖包<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid></dependency><dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-validation</artifactid></dependency>
注:从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。
注解功能
@assertfalse 可以为null,如果不为null的话必须为false
@asserttrue 可以为null,如果不为null的话必须为true
@decimalmax 设置不能超过最大值
@decimalmin 设置不能超过最小值
@digits 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内
@future 日期必须在当前日期的未来
@past 日期必须在当前日期的过去
@max 最大不得超过此最大值
@min 最大不得小于此最小值
@notnull 不能为null,可以是空
@null 必须为null
@pattern 必须满足指定的正则表达式
@size 集合、数组、map等的size()值必须在指定范围内
@email 必须是email格式
@length 长度必须在指定范围内
@notblank 字符串不能为null,字符串trim()后也不能等于“”
@notempty 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
@range 值必须在指定范围内
@url 必须是一个url
注:此表格只是简单的对注解功能的说明,并没有对每一个注解的属性进行说明;可详见源码。
1.2. 定义要参数校验的实体类@datapublic class validvo { private string id; @length(min = 6,max = 12,message = "appid长度必须位于6到12之间") private string appid; @notblank(message = "名字为必填项") private string name; @email(message = "请填写正确的邮箱地址") private string email; private string sex; @notempty(message = "级别不能为空") private string level;}
在实际开发中对于需要校验的字段都需要设置对应的业务提示,即message属性。
1.3. 定义校验类进行测试@restcontroller@slf4j@validatedpublic class validcontroller { @apioperation("requestbody校验") @postmapping("/valid/test1") public string test1(@validated @requestbody validvo validvo){ log.info("validentity is {}", validvo); return "test1 valid success"; } @apioperation("form校验") @postmapping(value = "/valid/test2") public string test2(@validated validvo validvo){ log.info("validentity is {}", validvo); return "test2 valid success"; } @apioperation("单参数校验") @postmapping(value = "/valid/test3") public string test3(@email string email){ log.info("email is {}", email); return "email valid success"; }}
这里我们先定义三个方法test1,test2,test3,
test1使用了@requestbody注解,用于接受前端发送的json数据,
test2模拟表单提交,
test3模拟单参数提交。
注意,当使用单参数校验时需要在controller上加上@validated注解,否则不生效。
1.4. 测试结果1test1的测试结果
发送值
post http://localhost:8080/valid/test1content-type: application/json{ "id": 1, "level": "12", "email": "47693899", "appid": "ab1c"}
返回值
提示的是org.springframework.web.bind.methodargumentnotvalidexception异常
{ "status": 500, "message": "validation failed for argument [0] in public java.lang.string com.jianzh6.blog.valid.validcontroller.test1(com.jianzh6.blog.valid.validvo) with 3 errors: [field error in object 'validvo' on field 'email': rejected value [47693899]; codes [email.validvo.email,email.email,email.java.lang.string,email]; arguments [org.springframework.context.support.defaultmessagesourceresolvable: codes [validvo.email,email]; arguments []; default message [email],[ljavax.validation.constraints.pattern$flag;@26139123,.*]; default message [不是一个合法的电子邮件地址]]...", "data": null, "timestamp": 1628239624332}
test2的测试结果
发送值
post http://localhost:8080/valid/test2content-type: application/x-www-form-urlencodedid=1&level=12&email=476938977&appid=ab1c
返回值
提示的是org.springframework.validation.bindexception异常
{ "status": 500, "message": "org.springframework.validation.beanpropertybindingresult: 3 errors\nfield error in object 'validvo' on field 'name': rejected value [null]; codes [notblank.validvo.name,notblank.name,notblank.java.lang.string,notblank]; arguments [org.springframework.context.support.defaultmessagesourceresolvable: codes [validvo.name,name]; arguments []; default message [name]]; default message [名字为必填项]...", "data": null, "timestamp": 1628239301951}
test3的测试结果
发送值
post http://localhost:8080/valid/test3content-type: application/x-www-form-urlencodedemail=476938977
返回值
提示的是javax.validation.constraintviolationexception异常
{ "status": 500, "message": "test3.email: 不是一个合法的电子邮件地址", "data": null, "timestamp": 1628239281022}
1.5. 问题虽然我们之前定义了全局异常拦截器,也看到了拦截器确实生效了,但是validator校验框架返回的错误提示太臃肿了,不便于阅读,为了方便前端提示,我们需要将其简化一下。
通过将参数异常加入全局异常来解决
1.6. 将参数异常加入全局异常直接修改之前定义的restexceptionhandler,单独拦截参数校验的三个异常:
javax.validation.constraintviolationexception,
org.springframework.validation.bindexception,
org.springframework.web.bind.methodargumentnotvalidexception,
@slf4j@restcontrolleradvicepublic class restexceptionhandler { /** * 默认全局异常处理。 * @param e the e * @return resultdata */ @exceptionhandler(exception.class) @responsestatus(httpstatus.internal_server_error) public resultdata<string> exception(exception e) { log.error("全局异常信息 ex={}", e.getmessage(), e); return resultdata.fail(returncode.rc500.getcode(),e.getmessage()); } @exceptionhandler(value = {bindexception.class, validationexception.class, methodargumentnotvalidexception.class}) public responseentity<resultdata<string>> handlevalidatedexception(exception e) { resultdata<string> resp = null; if (e instanceof methodargumentnotvalidexception) { // beanvalidation exception methodargumentnotvalidexception ex = (methodargumentnotvalidexception) e; resp = resultdata.fail(httpstatus.bad_request.value(), ex.getbindingresult().getallerrors().stream() .map(objecterror::getdefaultmessage) .collect(collectors.joining("; ")) ); } else if (e instanceof constraintviolationexception) { // beanvalidation get simple param constraintviolationexception ex = (constraintviolationexception) e; resp = resultdata.fail(httpstatus.bad_request.value(), ex.getconstraintviolations().stream() .map(constraintviolation::getmessage) .collect(collectors.joining("; ")) ); } else if (e instanceof bindexception) { // beanvalidation get object param bindexception ex = (bindexception) e; resp = resultdata.fail(httpstatus.bad_request.value(), ex.getallerrors().stream() .map(objecterror::getdefaultmessage) .collect(collectors.joining("; ")) ); } return new responseentity<>(resp,httpstatus.bad_request); } }
1.7. 测试结果2test1测试结果
发送值
post http://localhost:8080/valid/test1content-type: application/json{ "id": 1, "level": "12", "email": "47693899", "appid": "ab1c"}
接收值
{ "status": 400, "message": "名字为必填项; 不是一个合法的电子邮件地址; appid长度必须位于6到12之间", "data": null, "timestamp": 1628435116680}
2. 自定义注解虽然spring validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。
比如上面实体类中的sex性别属性,只允许前端传递传 m,f 这2个枚举值,如何实现呢?
2.1. 第一步,创建自定义注解@target({method, field, annotation_type, constructor, parameter, type_use})@retention(runtime)@repeatable(enumstring.list.class)@documented@constraint(validatedby = enumstringvalidator.class)//标明由哪个类执行校验逻辑public @interface enumstring { string message() default "value not in enum values."; class<?>[] groups() default {}; class<? extends payload>[] payload() default {}; /** * @return date must in this value array */ string[] value(); /** * defines several {@link enumstring} annotations on the same element. * * @see enumstring */ @target({method, field, annotation_type, constructor, parameter, type_use}) @retention(runtime) @documented @interface list { enumstring[] value(); }}
可以根据validator框架定义好的注解来仿写,基本上一致
2.2. 第二步,自定义校验逻辑public class enumstringvalidator implements constraintvalidator<enumstring, string> { private list<string> enumstringlist; @override public void initialize(enumstring constraintannotation) { enumstringlist = arrays.aslist(constraintannotation.value()); } @override public boolean isvalid(string value, constraintvalidatorcontext context) { if(value == null){ return true; } return enumstringlist.contains(value); }}
2.3. 第三步,在字段上增加注解@apimodelproperty(value = "性别")@enumstring(value = {"f","m"}, message="性别只允许为f或m")private string sex;
2.4. 第四步,体验效果post http://localhost:8080/valid/test2content-type: application/x-www-form-urlencodedid=1&name=javadaily&level=12&email=476938977@qq.com&appid=ab1cdddd&sex=n
{ "status": 400, "message": "性别只允许为f或m", "data": null, "timestamp": 1628435243723}
3. 分组校验一个vo对象在新增的时候某些字段为必填,在更新的时候又非必填。如上面的validvo中 id 和 appid 属性在新增操作时都是非必填,而在编辑操作时都为必填,name在新增操作时为必填,面对这种场景你会怎么处理呢?
在实际开发中我见到很多同学都是建立两个vo对象,validcreatevo,valideditvo来处理这种场景,这样确实也能实现效果,但是会造成类膨胀。
其实validator校验框架已经考虑到了这种场景并且提供了解决方案,就是分组校验,只不过很多同学不知道而已。
要使用分组校验,只需要三个步骤
3.1. 第一步,定义分组接口public interface validgroup extends default { interface crud extends validgroup{ interface create extends crud{ } interface update extends crud{ } interface query extends crud{ } interface delete extends crud{ } }}
这里我们定义一个分组接口validgroup让其继承javax.validation.groups.default,再在分组接口中定义出多个不同的操作类型,create,update,query,delete。
3.2. 第二步,在模型中给参数分配分组@datapublic class validvo { @null(groups = validgroup.crud.create.class) @notnull(groups = validgroup.crud.update.class, message = "应用id不能为空") private string id; @length(min = 6,max = 12,message = "appid长度必须位于6到12之间") @null(groups = validgroup.crud.create.class) @notnull(groups = validgroup.crud.update.class, message = "应用id不能为空") private string appid; @notblank(message = "名字为必填项") @notblank(groups = validgroup.crud.create.class,message = "名字为必填项") private string name; @email(message = "请填写正确的邮箱地址") private string email; @enumstring(value = {"f","m"}, message="性别只允许为f或m") private string sex; @notempty(message = "级别不能为空") private string level;}
给参数指定分组,对于未指定分组的则使用的是默认分组。
3.3. 第三步,给需要参数校验的方法指定分组 @postmapping(value = "/valid/add") public string add(@validated(value = validgroup.crud.create.class) validvo validvo){ log.info("validentity is {}", validvo); return "test3 valid success"; } @postmapping(value = "/valid/update") public string update(@validated(value = validgroup.crud.update.class) validvo validvo){ log.info("validentity is {}", validvo); return "test4 valid success"; }
这里我们通过value属性给add()和update()方法分别指定create和update分组
3.4. 测试post http://localhost:8080/valid/addcontent-type: application/x-www-form-urlencodedname=javadaily&level=12&email=476938977@qq.com&sex=f
create操作
在create时我们没有传递id和appid参数,校验通过。
{ "status": 100, "message": "操作成功", "data": "test3 valid success", "timestamp": 1652186105359}
update操作
使用同样的参数调用update方法时则提示参数校验错误
{ "status": 400, "message": "id不能为空; 应用id不能为空", "data": null, "timestamp": 1652186962377}
默认校验生效操作
由于email属于默认分组,而我们的分组接口validgroup已经继承了default分组,所以也是可以对email字段作参数校验的。
故意写错email格式
post http://localhost:8080/valid/addcontent-type: application/x-www-form-urlencoded/valid/update?name=javadaily&level=12&email=476938977&sex=f
{ "status": 400, "message": "请填写正确的邮箱地址; id不能为空; 应用id不能为空", "data": null, "timestamp": 1652187273865}
4. 业务规则校验业务规则校验指接口需要满足某些特定的业务规则,举个例子:业务系统的用户需要保证其唯一性,用户属性不能与其他用户产生冲突,不允许与数据库中任何已有用户的用户名称、手机号码、邮箱产生重复。
这就要求在创建用户时需要校验用户名称、手机号码、邮箱是否被注册;编辑用户时不能将信息修改成已有用户的属性。
最优雅的实现方法应该是参考 bean validation 的标准方式,借助自定义校验注解完成业务规则校验。
4.1. 自定义注解首先我们需要创建两个自定义注解,用于业务规则校验:
uniqueuser:表示一个用户是唯一的,唯一性包含:用户名,手机号码、邮箱
@documented@retention(runtime)@target({field, method, parameter, type})@constraint(validatedby = uservalidation.uniqueuservalidator.class)public @interface uniqueuser { string message() default "用户名、手机号码、邮箱不允许与现存用户重复"; class<?>[] groups() default {}; class<? extends payload>[] payload() default {};}
notconflictuser:表示一个用户的信息是无冲突的,无冲突是指该用户的敏感信息与其他用户不重合
@documented@retention(runtime)@target({field, method, parameter, type})@constraint(validatedby = uservalidation.notconflictuservalidator.class)public @interface notconflictuser { string message() default "用户名称、邮箱、手机号码与现存用户产生重复"; class<?>[] groups() default {}; class<? extends payload>[] payload() default {};}
4.2. 实现业务校验规则想让自定义验证注解生效,需要实现 constraintvalidator 接口。
接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类,因为需要校验多个参数,我们直接传入用户对象。需要提到的一点是 constraintvalidator 接口的实现类无需添加 @component 它在启动的时候就已经被加载到容器中了。
@slf4jpublic class uservalidation<t extends annotation> implements constraintvalidator<t, user> { protected predicate<user> predicate = c -> true; @resource protected userrepository userrepository; @override public boolean isvalid(user user, constraintvalidatorcontext constraintvalidatorcontext) { return userrepository == null || predicate.test(user); } /** * 校验用户是否唯一 * 即判断数据库是否存在当前新用户的信息,如用户名,手机,邮箱 */ public static class uniqueuservalidator extends uservalidation<uniqueuser>{ @override public void initialize(uniqueuser uniqueuser) { predicate = c -> !userrepository.existsbyusernameoremailortelphone(c.getusername(),c.getemail(),c.gettelphone()); } } /** * 校验是否与其他用户冲突 * 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 */ public static class notconflictuservalidator extends uservalidation<notconflictuser>{ @override public void initialize(notconflictuser notconflictuser) { predicate = c -> { log.info("user detail is {}",c); collection<user> collection = userrepository.findbyusernameoremailortelphone(c.getusername(), c.getemail(), c.gettelphone()); // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 return collection.isempty() || (collection.size() == 1 && collection.iterator().next().getid().equals(c.getid())); }; } }}
这里使用predicate函数式接口对业务规则进行判断。
4.3. 测试代码@restcontroller@requestmapping("/senior/user")@slf4j@validatedpublic class usercontroller { @autowired private userrepository userrepository; @postmapping public user createuser(@uniqueuser @valid user user){ user saveduser = userrepository.save(user); log.info("save user id is {}",saveduser.getid()); return saveduser; } @sneakythrows @putmapping public user updateuser(@notconflictuser @valid @requestbody user user){ user edituser = userrepository.save(user); log.info("update user is {}",edituser); return edituser; }}
使用很简单,只需要在方法上加入自定义注解即可,业务逻辑中不需要添加任何业务规则的代码。
post http://localhost:8080/valid/addcontent-type: application/json/senior/user{ "username" : "100001"}
{ "status": 400, "message": "用户名、手机号码、邮箱不允许与现存用户重复", "data": null, "timestamp": 1652196524725}
以上就是springboot参数校验validator框架怎么使用的详细内容。
