帮你少些一半参数校验代码的小技巧(重置版)

帮你少些一半参数校验代码的小技巧(重置版)

介绍

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个事情

  1. 将方法中的@Valid注解改为@Validated
  2. 在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