
介绍
github地址:https://github.com/erlieStar/spring-boot-validation-demo
几乎每个web网站都会对用户提交的参数进行校验,前端要做,后端也要做。防止用户直接通过接口调用的方式来请求或保存数据,从而导致产生脏数据等其他严重的后果。
因为有些校验的逻辑也很繁琐,为了减轻开发者的负担,Java发布了 JSR303/JSR-349数据校验规范
JSR303 是一项标准,JSR-349 是其的升级版本,添加了一些新特性,他们规定一些校验规范即校验注解,如 @Null,@NotNull,@Pattern,他们位于 javax.validation.constraints 包下,只提供规范不提供实现。而 hibernate validation 是对这个规范的实践(不要将 hibernate 和数据库 orm 框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如 @Length,@Range 等等,他们位于 org.hibernate.validator.constraints 包下。而万能的 spring 为了给开发者提供便捷,对 hibernate validation 进行了二次封装,显示校验 validated bean 时,你可以使用 spring validation 或者 hibernate validation,而 spring validation 另一个特性,便是其在 springmvc 模块中添加了自动校验,并将校验信息封装进了特定的类中。这无疑便捷了我们的 web 开发
在spring boot项目中只要加入如下依赖即可使用校验注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
查看子依赖会发现有如下依赖
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
JSR提供的部分校验注解如下
注解 解释 @Null 被注释的元素必须为 null @NotNull 被注释的元素必须不为 null @AssertTrue 被注释的元素必须为 true @AssertFalse 被注释的元素必须为 false @Min 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 @Max 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 @Pattern 被注释的元素必须符合指定的正则表达式,如验证手机号的正则表达式子,"^1[0-9]{10}#34; @Email 被注释的元素必须是电子邮箱地址
经常被搞混的3个注解
注解 解释 @NotNull 不能为null,但可以为empty @NotEmpty 不能为null,而且长度必须大于0 @NotBlank 只能作用在String上,不能为null,而且调用trim()后,长度必须大于0
我来举一个org.apache.commons.lang3.StringUtils中的例子,你就能理解NotBlank的意思了,如下断言都能测试通过
assertEquals(true,StringUtils.isBlank(null));
assertEquals(true,StringUtils.isBlank(""));
//空格
assertEquals(true,StringUtils.isBlank(""));
//回车
assertEquals(true,StringUtils.isBlank(""));
改造一个注册的接口
先定义一下状态枚举类
publicenumResponseCode{
SUCCESS(0,"成功"),
ERROR(1,"失败"),
ILLEGAL_ARGUMENT(2,"参数错误"),
EMPTY_RESULT(3,"结果为空"),
NEED_LOGIN(10,"需要登录");
privatefinalintcode;
privatefinalStringdesc;
ResponseCode(intcode,Stringdesc){
this.code=code;
this.desc=desc;
}
publicintgetCode(){
returncode;
}
publicStringgetDesc(){
returndesc;
}
}
定义项目的返回对象
@Data
/**注解的作用是序列化json时,如果是null对象,key也会消失*/
@JsonInclude(JsonInclude.Include.NON_NULL)
publicclassServerResponseimplementsSerializable{
/**状态值**/
privateintstatus;
/**描述**/
privateStringmsg;
/**数据**/
privateObjectdata;
publicServerResponse(intstatus,Stringmsg){
this.status=status;
this.msg=msg;
}
publicstaticServerResponsesuccess(){
returnnewServerResponse(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getDesc());
}
publicstaticServerResponseillegalArgument(Stringmsg){
returnnewServerResponse(ResponseCode.ILLEGAL_ARGUMENT.getCode(),msg);
}
}
注册接口,这里省略了一部分校验
@RequestMapping("regist")
publicServerResponseregist(@RequestParam("name")Stringname,
@RequestParam("phone")Stringphone,
@RequestParam("phone")Stringemail){
if(StringUtils.isBlank(name)){
returnServerResponse.illegalArgument("用户名不能为空");
}
//其他一堆校验过程,调用service
returnServerResponse.success();
}
当不满足条件时返回如下
{
"status":2,
"msg":"用户名不能为空"
}
当参数较多,校验的逻辑也越来越多,这时可以直接将前端传过来参数直接转为对象
@Data
publicclassStudent{
@NotBlank(message="用户名不能为空")
privateStringname;
@NotBlank(message="手机号不能为空")
@Pattern(regexp="^1(3|4|5|7|8)\\d{9}#34;,message="手机号码格式错误")
privateStringphone;
@NotBlank(message="邮箱不能为空")
@Email(message="邮箱格式错误")
privateStringemail;
}
@RequestMapping("regist")
publicServerResponseregist(@ValidStudentstudent,BindingResultbindingResult){
if(bindingResult.hasErrors()){
List<FieldError>errorList=bindingResult.getFieldErrors();
//list不为空
if(CollectionUtils.isNotEmpty(errorList)){
returnServerResponse.illegalArgument(errorList.get(0).getDefaultMessage());
}
}
//调用service
returnServerResponse.success();
}
代码将不满足条件的字段的描述取一个出来返回,类似如下。当都满足时才会返回成功
{
"status":2,
"msg":"用户名不能为空"
}
需要注意的地方如下
参数 Student 前需要加上@Valid或@Validated 注解(下文说这2个注解的区别),表明需要 spring 对其进行校验,而校验的信息会存放到其后的 BindingResult 中。注意,必须相邻,如果有多个参数需要校验,形式可以如下。foo(@Validated Student student, BindingResult studentBindingResult ,@Validated Bar bar, BindingResult barBindingResult); 即一个校验类对应一个校验结果。
@Validated比@Valid的功能更强大
@Validated比@Valid的功能更强大,主要体现在@Validated可以进行分组校验和嵌套校验。如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。
分组校验
定义分组校验的组
publicinterfaceValidateGroup{
interfaceRouteValidStart{}
interfaceRouteValidEnd{}
}
使用demo如下 RouteValidStart组只校验始发省id和详细地址 RouteValidEnd组只校验目的省id和详细地址
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
publicclassRoute{
@NotNull(groups={RouteValidStart.class},message="始发地省id不能为空")
privateIntegerstartProvinceId;
@NotNull(groups={RouteValidEnd.class},message="目的地省id不能为空")
privateIntegerendProvinceId;
@NotNull(groups={RouteValidStart.class,RouteValidEnd.class},message="详细地址不能为空")
privateStringaddress;
}
测试类如下
@Test
publicvoidtest(){
Routeroute=Route.builder().build();
StringerrorMsg=CommonUtil.getErrorResult(route,ValidateGroup.RouteValidStart.class);
//始发地省id不能为空详细地址不能为空
System.out.println(errorMsg);
route=Route.builder().startProvinceId(1).address("详细地址").build();
errorMsg=CommonUtil.getErrorResult(route,ValidateGroup.RouteValidStart.class);
//""
System.out.println(errorMsg);
route=Route.builder().address("详细地址").build();
errorMsg=CommonUtil.getErrorResult(route,ValidateGroup.RouteValidEnd.class);
//目的地省id不能为空
System.out.println(errorMsg);
}
校验工具类如下
publicclassCommonUtil{
publicstaticStringgetErrorResult(Objecto,Class<?>...groups){
StringBuildererrorMsg=newStringBuilder();
ValidatorFactoryvf=Validation.buildDefaultValidatorFactory();
Validatorvalidator=vf.getValidator();
Set<ConstraintViolation<Object>>set=validator.validate(o,groups);
for(ConstraintViolation<Object>constraintViolation:set){
errorMsg.append(constraintViolation.getMessage()).append("");
}
returnerrorMsg.toString();
}
}
当然在Controller中也可以使用,形式如下
@RequestMapping("addRoute")
publicServerResponseaddRoute(@Validated({RouteValidStart.class})Routeroute,BindingResultbindingResult)
嵌套校验
假如我们还要求学生填写父亲和母亲的名字(并且不能为空),而后端的设计中父亲和母亲的名字被放在另一个类Relation中,这就要求我们能进行嵌套校验。
我们要做如下2个事情
- 将方法中的@Valid注解改为@Validated
- 在relation属性上加@Valid注解
@Data
publicclassStudent{
@NotBlank(message="用户名不能为空")
privateStringname;
@NotBlank(message="手机号不能为空")
@Pattern(regexp="^1(3|4|5|7|8)\\d{9}#34;,message="手机号码格式错误")
privateStringphone;
@NotBlank(message="邮箱不能为空")
@Email(message="邮箱格式错误")
privateStringemail;
@NotNull(message="父母名字不能为空")
@Valid
privateRelationrelation;
}
@Data
publicclassRelation{
@NotBlank(message="父亲的姓名不能为空")
privateStringfatherName;
@NotBlank(message="父亲的姓名不能为空")
privateStringmotherName;
}
那Relation类的fatherName属性如何被赋值呢? 很简单,前端传入的参数名是如下即可
xxxx?relation.fatherName=li&relation.motherName=liu