JSR303参数校验

1、概述

SR 303 – Bean Validation 是一个数据验证的规范。

在任何时候,当你要处理一个应用程序的业务逻辑时,必须要考虑数据校验,确保输入进来的数据从语 义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余、不利于维护等问题。使用Bean Validation,将验证逻辑与相应的域模型进行绑定,能够很好的避免发生这样的问题。
Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。借助Hibernate Validator,可以很好的进行参数验证。

2、常用注解

validator内置注解:

  1. @Null:被注释的元素必须为null
  2. @NotNull:被注释的元素必须不为null
  3. @AssertTrue:被注释的元素必须为true
  4. @AssertFalse:被注释的元素必须为false
  5. @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  6. @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  7. @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  8. @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  9. @Size(max, min):被注释的元素的大小必须在指定的范围内
  10. @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
  11. @Past:被注释的元素必须是一个过去的日期
  12. @Future:被注释的元素必须是一个将来的日期
  13. @Pattern(value):被注释的元素必须符合指定的正则表达式

Hibernate Validator 附加的注解:

  1. @Email:被注释的元素必须是电子邮箱地址

  2. @Length:被注释的字符串的大小必须在指定的范围内

  3. @NotEmpty:被注释的字符串的必须非空

  4. @Range:被注释的元素必须在合适的范围内

  5. @NotBlank:验证字符串非null,且长度必须大于0

@NotNull,@NotEmpty和@NotBlank
关于@NotNull,@NotEmpty和@NotBlank之间的区别如以下

  • @NotNull 适用于任何类型,被标注的元素必须不能为null
  • @NotEmpty适用于String类型,Map类型或者数组,不能为null,且长度必须大于0
  • @NotBlank只能用于String类型,不能为null,且调用trim()后,长度必须大于0
    添加完需要的校验注解之后,在请求处理方法中的接收的请求参数上添加@Valid注解,即可开启校验功能 如果是分组校验则需要 @Validated 注解

3、基本使用

3.1 引入依赖

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.12</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!--单元测试-->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<!--json和bean转换依赖-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.66</version>
		</dependency>
		<!--包含时间,字符串等基本的工具类依赖-->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>

		<!--时间属性接收字符内串转date的依赖-->
		<dependency>
			<groupId>joda-time</groupId>
			<artifactId>joda-time</artifactId>
			<version>2.3</version>
		</dependency>

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

3.2 请求bean(RequestVo )注解标识

package com.st.valid.jsr3.simple.req;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.validator.constraints.Length;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.*;
import java.util.Date;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@lombok.Data
public class RequestVo {

    @NotNull(message = "id 不能为空")
    private Long id;

    @Min(value = 10,message = "年龄必须大于等于10岁")
    private Integer age;

    @Length(min = 5,max = 20,message = "名字的长度必须在5-10之间")
    private String name;

    @Email(message = "email format is error")
    private String email;

    @NotBlank(message = "地址不能为空")
    private String address;

    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$", message = "请填写正确的手机号")
    private String phone;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
    @Past(message = "必须是一个过去的时间")
    private Date  begin;

    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
    @Future(message = "必须是一个将来的时间")
    private Date  end;


}

3.3 controller

package com.st.valid.jsr3.simple.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.simple.req.RequestVo;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@RestController
public class VaildController {
    @PostMapping("/check")
    public String getData(@Valid @RequestBody RequestVo requestVo, BindingResult result){
        if(result.hasErrors()){
            Map<String,String> map = new HashMap<>();
            //1、获取校验的错误结果
            result.getFieldErrors().forEach((item)->{
                //FieldError 获取到错误提示
                String message = item.getDefaultMessage();
                //获取错误的属性的名字
                String field = item.getField();
                map.put(field,message);
            });
            return JSON.toJSONString(map);
        }
        return "ok";
    }

}

3.4 转换请求参数

主要是为了获取json的请求对象,然后发送请求测试

package com.st.valid.jsr3.simple.test;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.simple.req.RequestVo;
import org.junit.Test;
import java.util.Date;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
public class TT {
    @Test
    public void beanToJson(){
        RequestVo rv = new RequestVo();
        rv.setId(1L);
        rv.setAddress("上海市,普陀区,大渡河路888号");
        rv.setAge(12);
//        rv.setBegin(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
//        rv.setEnd(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
        rv.setBegin(new Date());
        rv.setEnd(new Date());
        rv.setPhone("15707066004");
        rv.setEmail("fangshengisli@163.com");
        rv.setName("fangsheng");
        String rvStr = JSON.toJSONString(rv);
        System.out.println(rvStr);
    }


}

运行测试方法获取json对象

{"address":"上海市,普陀区,大渡河路888号","age":12,"begin":1648976868584,"email":"fangshengisli@163.com","end":1648976868584,"id":1,"name":"fangsheng","phone":"15707066004"}

将上面的时间参数手动换成字符串的时间格式(最终请求参数)

{
    "address":"上海市,普陀区,大渡河路888号",
    "age":12,
    "begin":"2022-04-02 18:23:58",
    "email":"fangshengisli@163.com",
    "end":"2022-04-02 18:23:58",
    "id":1,
    "name":"fangsheng",
    "phone":"11707066004"
    }

3.5 测试

在这里插入图片描述

4、分组校验

4.1 分组场景需求分析

如上面的 基本使用 案列的基础上,如果请求bean RequestVo 既作为新增数据的bean(新增时id不存在) 也作为修改数据的bean(修改时候前端必须带id),我们不可能为了id需要两种转态而定义两个bean,我们这是个时候就可以使用分组校验的,新增是执行新增的校验逻辑,修改时就执行修改的校验逻辑,实现步骤如下

4.2 引入依赖

还是基本使用 案列的依赖

4.3 GroupRequestVo

package com.st.valid.jsr3.groupValid.entity;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@lombok.Data
public class GroupRequestVo {

    @NotNull(message = "修改必须指定id",groups = {UpdateGroup.class})
    @Null(message = "新增不能指定id",groups = {AddGroup.class})
    private Long id;

    @Min(value = 10, message = "年龄必须大于等于10岁",groups = {UpdateGroup.class,AddGroup.class})
    private Integer age;

    @Length(min = 5, max = 20, message = "名字的长度必须在5-10之间",groups = {UpdateGroup.class,AddGroup.class})
    private String name;

    @Email(message = "email format is error",groups = {UpdateGroup.class,AddGroup.class})
    private String email;

    @NotBlank(message = "地址不能为空",groups = {UpdateGroup.class})
    private String address;


}

4.4 定义两个分组

只需要定义两个接口就行,无需其他的操作,只是作为一个标识而已

package com.st.valid.jsr3.groupValid.mark;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
public interface AddGroup {
}

package com.st.valid.jsr3.groupValid.mark;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
public interface UpdateGroup {
}

4.5 controller

package com.st.valid.jsr3.groupValid.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.groupValid.entity.GroupRequestVo;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述: 这里要是指定了分组,实体类上的注解就是指定了分组的注解才生效,
 * 没有指定分组的默认不生效,要是没有指定分组,就是对没有指定分组的注解生效,指定分组的注解就不生效了
 * 可以在自定义的异常分组接口中继承Default类。所有没有写明group的都属于Default分组。
 */
@RestController
public class GroupVaildController {

    @PostMapping("/save")
    public String save(@Validated(AddGroup.class) @RequestBody GroupRequestVo grv, BindingResult result){
        String message = getError(result);
        return Objects.isNull(message)?"save ok":message;
    }

    @PostMapping("/update")
    public String update(@Validated(UpdateGroup.class) @RequestBody GroupRequestVo grv, BindingResult result){
        String message = getError(result);
        return Objects.isNull(message)?"update ok":message;
    }

    public String getError(BindingResult result){
        if(result.hasErrors()){
            Map<String,String> map = new HashMap<>();
            //1、获取校验的错误结果
            result.getFieldErrors().forEach((item)->{
                //FieldError 获取到错误提示
                String message = item.getDefaultMessage();
                //获取错误的属性的名字
                String field = item.getField();
                map.put(field,message);
            });
            return JSON.toJSONString(map);
        }
        return null;
    }

}

4.6 测试

4.6.1 新增请求

请求参数

{
    "id":1,
    "address":"上海市,普陀区,大渡河路888号",
    "age":1,"name":"fangsheng",
    "email":"fangshengisli@163.com"
}

在这里插入图片描述

4.6.2 修改请求

请求参数

{
    "address":"上海市,普陀区,大渡河路888号",
    "age":12,"name":"fangsheng",
    "email":"fangshengisli@163.com"
}

在这里插入图片描述

5、自定义校验

5.1 自定义场景需求

在大部分的情况下,以上的校验基本上能满足所有的校验需求,但是在极端的情况下默认提供的校验无法满足,我们就需要自定义的校验,假如我们有这样一种场景,校验的属性是一个int,这个值必须在一个集合规定的数据里面(当然这种场景也可以这则表达式解决)

5.2 CustomRequestVo

package com.st.valid.jsr3.customVaild.entity;
import com.st.valid.jsr3.customVaild.ListValue;
import com.st.valid.jsr3.groupValid.mark.AddGroup;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@lombok.Data
public class CustomRequestVo {

    /**
     * 显示状态[0-不显示;1-显示]可能是一个更大的范围 value = {0,1,2,3,4 ...}
     * @ListValue 接下来要定义的校验注解
     */
    @ListValue(value = {0,1}, groups = {AddGroup.class, UpdateGroup.class})
    private Integer showStatus;



}

5.3 自定义校验注解类 ListValue

package com.st.valid.jsr3.customVaild;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class}) // 校验器需要自定义用什么要得校验器 多个以逗号隔开
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) // 哪都可以标注
@Retention(RUNTIME)
public @interface ListValue {
    // 使用该属性去Validation.properties中取
    String message() default "{com.st.valid.jsr3.customVaild.ListValue.message}";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    // 数组,需要用户自己指定
    int[] value() default {};
}

5.4 自定义校验器 ListValueConstraintValidator

package com.st.valid.jsr3.customVaild;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 * @描述:
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {//<注解,校验值类型>
    // 存储所有可能的值
    private Set<Integer> set=new HashSet<>();

    // 初始化,你可以获取注解上的内容并进行处理
    @Override
    public void initialize(ListValue constraintAnnotation) {
        // 获取后端写好的限制 // 这个value就是ListValue里的value,我们写的注解是@ListValue(value={0,1})
        int[] value = constraintAnnotation.value();
        for (int i : value) {
            set.add(i);
        }
    }

    // 覆写验证逻辑
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        // 看是否在限制的值里
        return  set.contains(value);
    }
}

5.5 自定义报错配置类ValidationMessages.properties

默认回去当前的properties文件获取报错的信息,文件的名字不能变

com.st.valid.jsr3.customVaild.ListValue.message=value must in [0,1]

5.6 controller

package com.st.valid.jsr3.customVaild.controller;
import com.alibaba.fastjson.JSON;
import com.st.valid.jsr3.customVaild.entity.CustomRequestVo;
import com.st.valid.jsr3.groupValid.mark.UpdateGroup;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
 * @创建人: 放生
 * @创建时间: 2022/4/2
 */
@RestController
public class CustomVaildController {
    
    @PostMapping("/custom")
    public String custom(@Validated(UpdateGroup.class) @RequestBody CustomRequestVo grv, BindingResult result){
        String message = getError(result);
        return Objects.isNull(message)?"update ok":message;
    }

    public String getError(BindingResult result){
        if(result.hasErrors()){
            Map<String,String> map = new HashMap<>();
            //1、获取校验的错误结果
            result.getFieldErrors().forEach((item)->{
                //FieldError 获取到错误提示
                String message = item.getDefaultMessage();
                //获取错误的属性的名字
                String field = item.getField();
                map.put(field,message);
            });
            return JSON.toJSONString(map);
        }
        return null;
    }

}

5.7 测试

请求参数

{
    "showStatus":5
}

在这里插入图片描述

在这里插入图片描述

6、优化-统一异常处理

6.1 异常统一处理

上面所有的异常处理我们都是处理在每一个测试的controller中的,实际生产中是不行的,代码太冗余, @ ExceptionHandler 需要进行异常处理的方法必须与出错的方法在同一个Controller里面。那么当代码加入了 @ControllerAdvice,则不需要必须在同一个 controller 中了。这也是 Spring 3.2 带来的新特性。从名字上可以看出大体意思是控制器增强。 也就是说,@controlleradvice + @ ExceptionHandler 也可以实现全局的异常捕捉

(1)抽取一个异常处理类

@ControllerAdvice标注在类上,通过“basePackages”能够说明处理哪些路径下的异常。
@ExceptionHandler(value = 异常类型.class)标注在方法上

@Slf4j
@RestControllerAdvice(basePackages = "com.st.valid.jsr3.simple.controller")//管理的controller(这个包下的controller都会被管理)
public class ValidExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
    public R handleValidException(MethodArgumentNotValidException exception){

        Map<String,String> map=new HashMap<>();
        // 获取数据校验的错误结果
        BindingResult bindingResult = exception.getBindingResult();
        // 处理错误
        bindingResult.getFieldErrors().forEach(fieldError -> {
            String message = fieldError.getDefaultMessage();
            String field = fieldError.getField();
            map.put(field,message);
        });
        log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());

        return R.error(400,"数据校验出现问题").put("data",map);
    }
}

注意 如果进行统一的异常处理原来的controller中的BindingResult要注释掉,否则会导致@RestControllerAdvice不生效

在这里插入图片描述

6.2 异常信息国际化处理

@Slf4j
@RestControllerAdvice(basePackages = "com.st.valid.jsr3.simple.controller")//管理的controller(这个包下的controller都会被管理)
public class ValidExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
    public Map<String,String> handleValidException(MethodArgumentNotValidException exception){
        Map<String,String> map=new HashMap<>();
        // 获取数据校验的错误结果
        BindingResult bindingResult = exception.getBindingResult();
        //获取当前的国际化语言,
        ResourceBundle message = ResourceBundle.getBundle("messages", Locale.US);
        // 处理错误
        bindingResult.getFieldErrors().forEach(fieldError -> {
//            String message = fieldError.getDefaultMessage();
            String field = fieldError.getField();
            map.put(field,message.getString(field));
        });
        log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());

        return map;
    }


}

7、最终案例的目录结构

在这里插入图片描述


版权声明:本文为weixin_45041878原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。