数据校验-springboot

数据校验是Web开发中的重要部分,也是必须要考虑和面对的事情。


前置知识

先了解一下JSR、Hibernate Validator、Spring Validation

  • JSR(Java Specification Request)规范是Java EE 6中的一项子规范,也叫作Bean Validation。它指定了一整套基于bean的验证API,通过标注给对象属性添加约束条件

  • Hibernate Validator是对JSR规范的实现,并增加了一些其他校验注解,如@Email、@Length、@Range等。

    • @Valid
  • Spring Validation是Spring为了给开发者提供便捷,对Hibernate Validator进行了二次封装。

    • @Validated,对@Valid进行了二次封装

JSR定义了数据验证规范,而Hibernate Validator则是基于JSR规范,实现了各种数据验证的注解以及一些附加的约束注解。Spring Validation则是对Hibernate Validator的封装整合。

Spring Boot是从Spring发展而来的,所以自然支持Hibernate Validator和Spring Validation两种方式,默认使用的是Hibernate Validator组件。


Spring Boot添加maven依赖如下:

<!--        参数校验-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

常用注解

所有的注解都包含code和message这两个属性(见后边的代码result.getFieldErrors())。Message定义数据校验不通过时的错误提示信息。code定义错误的类型。


扩展

@NotNull、@NotEmpty、@NotBlank区别:

  • 1、@NotNull

    • 不能为 null,但可以为 empty,一般用在 int、long、double、BigDecimal等数值类型的非空校验上,而且被其标注的字段可以使用 @size、@Max、@Min 对字段数值进行大小的控制
    • @Range 一般用在 数值 类型上可对字段数值进行大小范围的控制。
  • 2、@NotEmpty

    • 不能为 null,且长度必须大于 0,一般用在 集合 类上或者 数组 上
  • 3、@NotBlank

    • 只能作用在接收的 String 类型上,不能为 null,而且调用 trim() 后,长度必须大于 0即:必须有实际字符
    • @Length 一般用在 String 类型上可对字段数值进行最大长度限制的控制。

案例1-bean中进行数据校验

post请求参数较多时,可以在对应的数据模型(Java Bean)中进行数据校验,通过注解来指定字段校验的规则。

1、定义数据模型

定义一个接收数据的数据模型(javabean),使用注解的形式描述字段校验的规则。下面以PostTag_form对象为例:

/**
 * <p>
 * 接收前端提交的表单数据
 * </p>
 *
 * @author 轻率的保罗
 * @since 2022-12-17
 */
@Getter
@Setter
@Accessors(chain = true)
@ApiModel(value = "标签form对象", description = "前端提交的表单数据")
public class PostTag_form implements Serializable {
    
    

    @ApiModelProperty(value = "标签名",required = true)
    @NotBlank(message = "标签名称不能为空!")
    @Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!")
    private String tagName;

    @ApiModelProperty(value = "创建者ID",required = true)
//  @NotBlank(message = "创建者ID不能为空!")
    @Length(min = 5,max = 25,message = "创建者ID长度不合法,应大于等于5、小于等于25!")//包含了 @NotBlank 的情况
    private String userId;

}

注解@NotBlank,表示该字段必填(该字符串不能为空)

注解中的属性message是数据校验不通过时要给出的提示信息

扩展

@NotNull(message = "年龄不能为空!")
@Min(18)
private int age;

@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误")
private String phone;

@Email(message = "邮箱格式错误")
private String email;

@Size(min = 3,max = 5,message = "list的Size在[3,5]")
private List<String> list;

2、应用

在controller中应用:

post请求中,@RequestBody PostTag_form form接收前端所提交的数据。在@RequestBody注解后面添加了@Validated注解,然后在后面添加了BindingResult返回验证结果,BindingResult是验证不通过时的结果集合。

@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form, BindingResult result){
    
    
    //参数校验
    if(result.hasErrors()) {
    
    
        return JSONResult.errorMap(result);
    }
    //参数校验通过,再调用service的addTag方法处理请求
    return tagService.addTag(form);
}

注意,BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,则会抛出BindException。

JSONResult 中添加方法 errorMap(BindingResult result) 如下:

/**
 * 异常 代码501
 * 参数异常,map中放具体的异常参数(key/value)。例如 email = 邮箱格式不正确!
 * @param result springboot参数校验(validation),返回BindingResult对象。
 */
public static DataVo errorMap(BindingResult result) {
    
    
    HashMap<String, String>  errMap = new HashMap<>(result.getFieldErrors().size());
    for (FieldError error : result.getFieldErrors()) {
    
    
        //示例: "tagName": "不能为空"
        //error.getCode() 可获取校验不通过的注解,即@NotBlank 还是 @Length
        errMap.put(error.getField(),error.getDefaultMessage());
    }
    return new DataVo(501, "参数异常!详情见data!", errMap);
}

扩展

前边的示例中,参数校验结果保存在BindingResult中,若存在校验不通过的情况还需要我们手动调用JSONResult.errorMap()返回异常数据给前端。若每个接口都这样处理感觉还是比较麻烦的,那有没有更优雅的处理方式呢?

我们可以利用“没有BindingResult对象会抛出BindException异常”,然后在全局异常处理类中捕获处理。

controller中修改方法addTag():

@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form){
    
    
    return tagService.addTag(form);
}

全局异常处理类中添加方法handlerViolationException():

/**
 * 数据校验异常处理 - MethodArgumentNotValidException(继承BindException)
 * controller中方法上配置数据校验(参数为javabean,校验规则配置在bean中),校验不通过会抛出异常MethodArgumentNotValidException
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handlerViolationException(MethodArgumentNotValidException e) {
    
    
    HashMap<String, String> errMap = new HashMap<>(e.getFieldErrors().size());
    for (FieldError error : e.getFieldErrors()) {
    
    
        //示例: "tagName": "不能为空"
        errMap.put(error.getField(),error.getDefaultMessage());
    }
    e.printStackTrace();
    String message = ExceptionUtils.getMessage(e);
    // 记录日志
    log.error(message);
    return JSONResult.build(501,"参数异常,详情见data!",errMap);
}

使用 全局异常处理类 来捕获数据校验不通过时抛出的异常,更加方便。


3、测试

提交空数据

请求参数如下:

{
    
    
  "tagName": "",//长度0
  "userId": ""//长度0
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常!详情见data!",
  "data": {
    
    
    "tagName": "标签名称不能为空!",
    "userId": "创建者ID长度不合法,应大于等于5、小于等于25!"
  }
}

提交错误数据(超长度)

请求参数如下:

{
    
    
  "tagName": "123451234512345123451",//长度21
  "userId": "1234"//长度4
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常!详情见data!",
  "data": {
    
    
    "tagName": "标签名称长度不合法,应大于0、小于等于20!",
    "userId": "创建者ID长度不合法,应大于等于5、小于等于25!"
  }
}

案例2-URL参数校验

一般GET请求都是在URL中传入参数。对于这种情况,可以直接通过注解来指定参数的校验规则。

@Validated
。。。
public class SysTagController {
    
    
 
  @ApiOperation(value = "删除标签图片")
  @DeleteMapping("/img/{tagId}")
  public DataVo<String> delTagImg(@PathVariable 
                                  @ApiParam("标签ID") 
                                  @Length(max = 25, message = "标签ID长度不合法,应小于等于25!") String tagId){
    
    
      //测试,演示方法中有一个参数添加数据校验的情况
      return JSONResult.ok();
  }

  @ApiOperation(value = "获取标签信息")
  @GetMapping
  public DataVo<String> getTag(@ApiParam(value = "标签ID",required = true) 
                               @NotBlank 
                               @Length(max = 25, message = "标签ID长度不合法,应小于等于25!") String tagId,
                               @ApiParam(value = "标签名称",required = true) 
                               @NotBlank 
                               @Length(max = 20, message = "标签名称长度不合法,应小于等于20!") String tagName){
    
    
      //测试,演示方法中有多个参数添加数据校验的情况
      return JSONResult.ok();
  }
  
}

注意:

1、使用注解对URL中传入的参数进行校验。

2、在方法所在的控制器controller上添加@Validated注解来使得验证生效(方法的参数前不用添加注解@Validated)。

全局异常处理类中添加方法handlerViolationException():

/**
 * 数据校验异常处理 - ConstraintViolationException
 * controller中方法上配置数据校验(参数为"基本数据类型",校验规则配置在该参数前),校验不通过会抛出异常ConstraintViolationException
 */
@ExceptionHandler(ConstraintViolationException.class)
public Object handlerViolationException(ConstraintViolationException e) {
    
    
    //e.getMessage() = getTag.tagId: 不能为空, getTag.tagName: 不能为空
    //保存到map中: tagId = 不能为空
    HashMap<String, String> errMap = new HashMap<>();
    Arrays.stream(e.getMessage().split(",")).forEach( msg->errMap.put(msg.substring(msg.indexOf(".")+1,msg.indexOf(":")),msg.substring(msg.indexOf(":")+2)) );
    e.printStackTrace();
    String message = ExceptionUtils.getMessage(e);
    // 记录日志
    log.error(message);
    return JSONResult.build(501,"参数异常,详情见data!",errMap);
}

测试

提交空数据

请求参数如下:

?tagId=%20&tagName=%20

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常!详情见data!",
  "data": {
    
    
    "tagId": "不能为空",
    "tagName": "不能为空"
  }
}

提交错误数据(超长度)

请求参数如下:

//接口“删除标签图片”
?tagId=01234567890123456789012345

//接口“获取标签信息”
?tagId=01234567890123456789012345&tagName=%20

响应内容:

//接口“删除标签图片”
{
    
    
  "code": 501,
  "msg": "参数异常!详情见data!",
  "data": {
    
    
    "tagId": "标签ID长度不合法,应小于等于25!"
  }
}

//接口“获取标签信息”
{
    
    
  "code": 501,
  "msg": "参数异常!详情见data!",
  "data": {
    
    
    "tagId": "标签ID长度不合法,应小于等于25!",
    "tagName": "不能为空"
  }
}

案例3-bean对象级联校验

对于JavaBean对象中的普通属性字段,我们可以直接使用注解进行数据校验(见前边案例1),那如果是关联对象呢?其实也很简单,在属性上添加@Valid注解就可以作为属性对象的内部属性进行验证。

例如:前边案例1中在类PostTag_form中添加变量TagIconDetail iconDetail

public class PostTag_form implements Serializable {
    
    

    @ApiModelProperty(value = "标签名",required = true)
    @NotBlank(message = "标签名称不能为空!")
    @Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!")
    private String tagName;

    @ApiModelProperty(value = "创建者ID",required = true)
    @NotBlank(message = "创建者ID不能为空!")
    @Length(max = 25,message = "创建者ID长度不合法,应大于0、小于等于25!")
    private String userId;

    @Valid
    private TagIconDetail iconDetail;
}

TagIconDetail如下:

@Getter
@Setter
@Accessors(chain = true)
@ApiModel(value = "TagIconDetail对象", description = "标签各尺寸图标信息!")
public class TagIconDetail implements Serializable {
    
    

    @ApiModelProperty(value = "高分屏应用图标",required = true)
    @NotBlank(message = "高分屏应用图标 不能为空!")
    private String icon72;

    @ApiModelProperty(value = "720P高分屏应用图标",required = true)
    @NotBlank(message = "720P高分屏应用图标 不能为空!")
    private String icon96;

    @ApiModelProperty(value = "1080P高分屏应用图标",required = true)
    @NotBlank(message = "1080P高分屏应用图标 不能为空!")
    private String icon144;

}

测试

请求参数:

{
    
    
  "iconDetail": {
    
    
    "icon144": "",
    "icon72": "",
    "icon96": ""
  },
  "tagName": "ffgegwergaerfegqertgqerfqerg",
  "userId": ""
}

响应内容:

可以看到,新增加的变量iconDetail,由于添加了@Valid注解故也校验其内部配置了校验规则的属性(iconDetail.icon72、iconDetail.icon96等)

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "iconDetail.icon72": "高分屏应用图标 不能为空!",
    "iconDetail.icon96": "720P高分屏应用图标 不能为空!",
    "iconDetail.icon144": "1080P高分屏应用图标 不能为空!",
    "tagName": "标签名称长度不合法,应大于0、小于等于20!",
    "userId": "创建者ID不能为空!"
  }
}

案例4-分组校验

在不同情况下,可能对JavaBean对象的数据校验规则有所不同,有时需要根据数据状态对JavaBean中的某些属性字段进行单独验证。这时就可以使用分组校验功能,即根据状态启用一组约束。

Hibernate Validator的注解提供了groups参数,用于指定分组,如果没有指定groups参数,则默认属于javax.validation.groups.Default分组。

创建分组

创建分组Group_A、Group_B

/**
 * springboot数据校验,分组校验,分组A
 */
public interface Group_A {
    
    
}

/**
 * springboot数据校验,分组校验,分组B
 */
public interface Group_B {
    
    
}

定义了Group_A和Group_B两个接口作为两个校验规则的分组。

应用

bean中定义

还是前边案例中的PostTag_form,在相关的字段中定义校验分组规则。

...
public class PostTag_form implements Serializable {
    
    

    @ApiModelProperty(value = "标签名",required = true)
    @NotBlank(message = "标签名称不能为空!")
    @Length(max = 20,message = "标签名称长度不合法,应大于0、小于等于20!",groups = {
    
    Group_A.class})
    @Pattern(regexp = "[^0-9]+", message = "不能包含数字!",groups = {
    
    Group_B.class})
    private String tagName;

    @ApiModelProperty(value = "创建者ID",required = true)
    @NotBlank(message = "创建者ID不能为空!")
    @Length(max = 25,message = "创建者ID长度不合法,应大于0、小于等于25!")//默认分组 javax.validation.groups.Default.class
    private String userId;

}
  • tagName字段定义了Group_A和Group_B两个分组校验规则。
    • Group_A的校验规则限制长度
    • Group_B的校验规则限制不能包含数字
  • userId字段中,不定义分组校验规则,默认属于Default分组。

controller中使用

使用校验分组。

在@Validated注解中增加了{Group_A.class}参数,表示对于定义了分组校验的字段使用Group_A校验规则。

@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated({
    
    Group_A.class}) PostTag_form form){
    
    
    return tagService.addTag(form);
}

若要同时使用分组A与B,这样配置:@Validated({Group_A.class,Group_B.class}),使用逗号分隔开。


测试

1.应用分组Group_A

...
public DataVo addTag(@RequestBody @Validated({
    
    Group_A.class}) PostTag_form form){
    
    
...
}

请求参数:

{
    
    
  "tagName": "abcde1abcde2abcde3abcde",//长度超20且包含数字
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "tagName": "标签名称长度不合法,应大于0、小于等于20!"
  }
}

应用分组Group_A,所以只有bean中tagName字段才进行数据校验(参数中userId字段没有数据,也不提示不能为空)。

分组A,tagName字段限制字符长度。

2.应用分组Group_B

...
public DataVo addTag(@RequestBody @Validated({
    
    Group_B.class}) PostTag_form form){
    
    
...
}

请求参数:

{
    
    
  "tagName": "abcde1abcde2abcde3abcde",//长度超20且包含数字,与前边示例1一样
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "tagName": "不能包含数字!"
  }
}

应用分组Group_B,所以只有bean中tagName字段才进行数据校验(参数中userId字段没有数据,也不提示不能为空)。

分组B,tagName字段限制不能包含数字。

3.同时应用多个分组

上边两个测试中,我们注意到userId字段配置的校验规则没有生效。是因为userId字段不定义分组校验规则,默认属于Default分组。若想在应用分组A(或B)的同时,默认分组的校验规则也生效,就需要同时应用多个分组。

...
public DataVo addTag(@RequestBody @Validated({
    
    Group_A.class, Default.class}) PostTag_form form){
    
    
...
}

请求参数:

{
    
    
  "tagName": "abcde1abcde2abcde3abcde",//长度超20且包含数字,与前边示例1、2一样
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "tagName": "标签名称长度不合法,应大于0、小于等于20!",
    "userId": "创建者ID不能为空!"
  }
}

分组A,tagName字段限制字符长度。

默认分组,userId字段限制必填。

注意:同时应用多个分组,各个分组的校验默认是没有顺序,但有些时候是需要按我们指定的顺序来进行校验,那么如何让各个分组按顺序进行校验呢?可以通过“组序列”来实现。


组序列

一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证

定义组序列Group_seq:

/**
 * springboot数据校验,分组校验,分组序列Group_seq。
 * 先校验Group_A,再校验Group_B,最后校验默认分组Default(javax.validation.groups.Default)
 */
@GroupSequence({
    
    Group_A.class, Group_B.class, Default.class})
public interface Group_seq {
    
    
}

测试

请求参数:

{
    
    
  "tagName": "abcde1abcde2abcde3abcde",//长度超20且包含数字,与前边示例1、2、3一样
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "tagName": "标签名称长度不合法,应大于0、小于等于20!"
  }
}

分组A,tagName字段限制字符长度。分组A校验不通过,不再校验分组B、默认分组

请求参数:

{
    
    
  "tagName": "abcde1abcde2abcde3ab",//长度为20(删掉后边cde)且包含数字
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "tagName": "不能包含数字!"
  }
}

分组A,tagName字段限制字符长度。

分组B,tagName字段限制不能包含数字。

分组A校验通过,分组B校验不通过,不再校验默认分组

请求参数:

{
    
    
  "tagName": "abcdeabcdeabcdeab",//长度为17(删掉后边cde和数字)且不包含数字
  "userId": ""
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "userId": "创建者ID不能为空!"
  }
}

分组A,tagName字段限制字符长度。

分组B,tagName字段限制不能包含数字。

默认分组,userId字段限制必填。

分组A校验通过,分组B校验通过,默认分组校验不通过。按顺序依次验证各个分组。


案例5-自定义校验

通过自定义校验规则,可以实现一些复杂、特殊的数据验证功能。

定义校验注解

定义新的校验注解@CustomAgeValidator,示例代码如下:

@Min(value = 18,message = "年龄最小不能小于18")
@Max(value = 120,message = "年龄最大不能超过120")
@Constraint(validatedBy = {
    
    }) //不指定校验器
@Documented
@Target({
    
    ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomAgeValidator {
    
    
    String message() default "年龄大小必须大于18并且小于120";
    Class<?>[] groups() default {
    
    };
    Class<? extends Payload>[] payload() default {
    
    };
}

创建了CustomAgeValidator自定义注解,用于自定义年龄的数据校验规则。

使用

在前边案例的bean中添加age变量,添加自定义校验注解@CustomAgeValidator:

@CustomAgeValidator
private int age;

controller如下:

@ApiOperation(value = "新增标签",notes = "创建成功,则返回新创建标签的ID")
@PostMapping
public DataVo addTag(@RequestBody @Validated PostTag_form form){
    
    
    return tagService.addTag(form);
}

测试

请求参数:

{
    
    
  "age": 8,
  "tagName": "xxx",
  "userId": "xxx"
}

响应内容:

{
    
    
  "code": 501,
  "msg": "参数异常,详情见data!",
  "data": {
    
    
    "age": "年龄最小不能小于18"
  }
}

自定义校验注解@CustomAgeValidator中,限制了年龄最小不能小于18,请求参数中设置age=8,校验不通过!


说明

本博客中的案例,使用的maven依赖如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<!--        启用web支持-->
<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>

<!--        启用lombok(lombok 与 日志@Slf4j)-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<!--        启用单元测试-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- mybatis-plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.2</version>
</dependency>


笔记摘自:CSDN-故屿γCSDN-王大地X《Spring Boot从入门到实战》-章为忠

猜你喜欢

转载自blog.csdn.net/weixin_44773109/article/details/128374809