添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
无聊的生姜  ·  DDP num_workers>0 ...·  2 月前    · 
有腹肌的啄木鸟  ·  Git 打补丁 | ...·  2 月前    · 
谈吐大方的山楂  ·  Linux ...·  3 月前    · 
  • 在平常的项目开发中,我们常需要校验前端传递的参数是否合法
  • 对于是新增、修改的数据,校验参数是否合法的意义在于 防止异常的数据落到数据库层导致数据库异常
  • 对于是查询的数据,校验参数是否合法的意义在于 过滤掉一些明显不合法的查询条件,防止恶意查询,减少数据库的查询压力
  • @PostMapping("/add")
    private ApiResponse<String> add(@RequestBody ApiRequest<PersonDto> apiRequest) {
        PersonDto person = apiRequest.getData();
        //入参校验的常规做法
        String name = person.getName();
        if(null == name || "".equals(name) || name.length() > 20) {
            return ApiResponse.error(4000, "入参校验不通过: name不合法");
        Integer age = person.getAge();
        if(null == age || age < 0 || age > 100) {
            return ApiResponse.error(4000, "入参校验不通过: age不合法");
        //...
        //personService.add(person);
        return ApiResponse.success(2000, "成功");
    

    本文主要内容

  • 介绍两种参数校验方式(valid与validated)的基本用法
  • 提供相应的测试案例,供读者深度理解
  • @Valid
  • 校验表单实体
  • 校验List实体
  • @Validated
  • 校验失败的异常
  • 拦截处理异常
  • JSON实体类
  • 表单实体类
  • 校验单个参数
  • 数组与嵌套
  • @Valid

  • @Valid是使用Hibernate validation的时候使用。注意:Java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现。
  • 可以用在方法、构造函数、方法参数和成员属性(字段)上
  • 支持嵌套检测:在一个beanA中,存在另外一个beanB属性。嵌套检测beanA同时也检测beanB
  • 不支持分组
  • @Valid 进行校验的时候,需要用 BindingResult 来做一个校验结果接收。当校验不通过的时候,如果手动不 return ,则并不会阻止程序的执行;
  • @Validated

  • @Validated是只用Spring Validator校验机制使用
  • 可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上
  • 不支持嵌套检测
  • @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要再写一个全局校验异常捕获处理类,然后返回校验提示。
  • 与SpringBoot整合

  • 在SpringBootv2.3之前的版本只需要引入 web 依赖就可以了,他包含了validation校验包
  • 而在此之后SpringBoot版本就独立出来了需要单独引入依赖
  • <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    注意:这个依赖的本质还是
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>5.4.1.Final</version>
    </dependency>
    

    字符串空值检查

  • @NotBlank(message =) 验证字符串非null,且长度必须大于0
  • @NotEmpty 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“”
  • @NotNull 不能为null,可以为空
  • @Null 必须为null
  • 字符串格式检查

  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是一个有效的url
  • @DecimalMax(value=x) 被注解的元素值小于等于(<=)指定的十进制value 值
  • @DecimalMin(value=x) 被验证注解的元素值大于等于指定的十进制value 值
  • @Digits(integer=整数位数, fraction=小数位数) 验证注解的元素值的整数位数和小数位数上限
  • @Max(value=x) 验证注解的元素值小于等于指定的 value值
  • @Min(value=x) 验证注解的元素值大于等于指定的 value值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内,可以是 String、Collection、Map、数组
  • @Range 值必需在一个范围内
  • @Positive:被注释的元素必须为正数
  • @PositiveOrZero:被注释的元素必须为正数或 0
  • @Negative:被注释的元素必须为负数
  • @NegativeOrZero:被注释的元素必须为负数或 0
  • @Future 验证日期是否是未来
  • @FutureOrPresent:被注释的元素必须是现在或者将来的日期
  • @Past 验证日期是否是已经过去了的
  • @PastOrPresent:被注释的元素必须是现在或者过去的日期
  • @Null 验证元素必须为 null
  • @NotNull 验证元素必须不为 null
  • @AssertTrue 验证元素必须为 true
  • @AssertFalse 验证元素必须为 false
  • @Data
    public class StudentAddDto {
        @NotBlank(message = "主键不能为空")
        private String id;
        @NotBlank(message = "名字不能为空")
        @Size(min=2, max = 4, message = "名字字符长度必须为 2~4个")
        private String name;
        @Pattern(regexp = "/^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/", message = "手机号格式错误")
        private String phone;
        @Email(message = "邮箱格式错误")
        private String email;
        @Past(message = "生日必须早于当前时间")
        private Date birth;
        @Min(value = 0, message = "年龄必须为 0~100")
        @Max(value = 100, message = "年龄必须为 0~100")
        private Integer age;
        @PositiveOrZero
        private Double score;
    

    @Valid

  • 在Controller层使用”@Valid”修饰实体类参数
  • 在实体类的属性上使用校验注解
  • 使用BindingResult接收校验结果,手动控制是否校验通过
  • import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    import javax.validation.constraints.*;
    import java.math.BigDecimal;
    import java.util.Date;
    @Data
    public class PersonAddDto {
        private Long id;
        @NotBlank(message = "请输入名称")
        @Size(message = "名称字符长度在{min}到{max}之间", min = 1, max = 10)
        private String name;
        @NotNull(message = "请输入年龄")
        @Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
        private Integer age;
        @Max(value = 2, message = "性别限定最大值为2")
        @Min(value = 1, message = "性别限定最小值为1")
        @Positive(message = "性别字段只可能为正数")
        private Integer gender;
        @PastOrPresent(message = "出生日期一定在当前之间之前")
        private Date birthday;
        private String address;
        @Pattern(regexp = "/^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\\d{8}$/", message = "手机号格式错误")
        private String tel;
        @Email(message = "邮箱格式错误")
        private String email;
        @DecimalMax(value = "99999", message = "工资上限为99999")
        //@DecimalMin(value = "99", message = "工资下限为99")
        @PositiveOrZero
        private BigDecimal salary;
    

    校验表单实体

  • 在Controller层使用”@Valid”修饰实体类参数
  • 在实体类中使用校验注解
  • 使用BindingResult接收校验结果
  • import com.alibaba.fastjson.JSON;
    import com.ks.demo.vv.dto.ApiResponse;
    import com.ks.demo.vv.dto.PersonAddDto;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import javax.validation.Valid;
    @RequestMapping("/valid")
    @RestController
    public class ValidController {
        @PostMapping("/addByFrom")
        private ApiResponse<String> addByFrom(@Valid PersonAddDto personAddDtoDto, BindingResult bindingResult) {
            if(bindingResult.hasErrors()) {
                //return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
                StringBuilder sb = new StringBuilder();
                bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
                return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
            System.out.println(JSON.toJSONString(personAddDtoDto));
            return ApiResponse.success(2000, "成功");
    

    校验List实体

  • 在Controller层使用”@Valid List<PersonAddDto> personDtoList”,是无法检测List中的实体属性的
  • 最佳解决方案:@RequestBody @Valid ValidList<PersonAddDto> personDtoList,也即将List包一层,并使用@Valid修饰该个List
  • import com.alibaba.fastjson.JSON;
    import com.ks.demo.vv.dto.ApiResponse;
    import com.ks.demo.vv.dto.PersonAddDto;
    import com.ks.demo.vv.dto.ValidList;
    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.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import javax.validation.Valid;
    import java.util.List;
    @RequestMapping("/valid")
    @RestController
    public class ValidController {
         * 校验失效,不支持检验List中的实体
        @PostMapping("/add")
        private ApiResponse<String> add(@RequestBody @Valid List<PersonAddDto> personDtoList, BindingResult bindingResult) {
            if(bindingResult.hasErrors()) {
                //return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
                StringBuilder sb = new StringBuilder();
                bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
                return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
            System.out.println(JSON.toJSONString(personDtoList));
            return ApiResponse.success(2000, "成功");
         * 解决校验List中的实体:使用一个对象包装一层List,其本质是"嵌套校验"
         * 最佳的解决方案是下文将要介绍的将请求体以ApiRequest封装,在"T data"上使用@Valid修饰
        @PostMapping("/addListByNest")
        private ApiResponse<String> addListByNest(@RequestBody @Valid ValidList<PersonAddDto> personDtoList, BindingResult bindingResult) {
            if(bindingResult.hasErrors()) {
                //return ApiResponse.error(9999, "请求参数校验不通过", JSON.toJSONString(bindingResult.getAllErrors()));
                StringBuilder sb = new StringBuilder();
                bindingResult.getAllErrors().forEach(ele -> sb.append(ele.getDefaultMessage()).append(";"));
                return ApiResponse.error(9999, "请求参数校验不通过", sb.toString());
            System.out.println(JSON.toJSONString(personDtoList));
            return ApiResponse.success(2000, "成功");
    import lombok.Data;
    import javax.validation.Valid;
    import java.util.List;
    @Data
    public class ValidList<T> {
        @Valid
        private List<T> dataList;
    

    嵌套检测:在一个beanA中,存在另外一个beanB属性。嵌套检测beanA同时也检测beanB

    * 嵌套检测 * 重点关注ApiRequest中"private T data;"上的"@Valid" @PostMapping("/addByObjNest") private ApiResponse<String> addByObjNest(@RequestBody @Valid ApiRequest<PersonAddDto> personDto, BindingResult br) { if(br.hasErrors()) { return ApiResponse.error(9999, "请求参数校验不通过", errorInfo(br)); System.out.println(JSON.toJSONString(personDto)); return ApiResponse.success(2000, "成功"); * 嵌套List @PostMapping("/addByListNest") private ApiResponse<String> addByListNest(@RequestBody @Valid ApiRequest<List<PersonAddDto>> personDto, BindingResult br) { if(br.hasErrors()) { return ApiResponse.error(9999, "请求参数校验不通过", errorInfo(br)); System.out.println(JSON.toJSONString(personDto)); return ApiResponse.success(2000, "成功");

    @Validated

  • 在Controller层使用”@Validated”修饰实体类参数
  • 在实体类的属性上使用校验注解
  • 立即失败:一旦校验失败,自动抛出异常(springframework.validation.BindException),结束正在执行中的流程,本个HTTP请求结束,响应500
  • 校验的参数是实体,@Validated注解直接放在该模型参数前即可,属性校验放在实体类的属性上。
  • 在实体类的属性上加验证注解
  • 校验的参数是普通参数,@Validated要直接放在类上,在具体的参数前加上校验注解。
  • 校验失败的异常

    背景:由于校验不通过则立即失败,抛出异常,本次请求结束,响应500。

    异常抛出:

  • 校验从@RequestBody来的实体,失败抛出: org.springframework.web.bind.MethodArgumentNotValidException
  • 校验普通实体失败抛出:springframework.validation.BindException
  • 校验普通参数失败抛出:validation.ConstraintViolationException
  • import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import net.hackyle.boot.entity.Person;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    import javax.validation.constraints.Email;
    @Api("ViolationController")
    @Validated
    @RestController
    public class ViolationController {
        @ApiOperation(value = "test01", notes = "校验从@RequestBody来的实体")
        //校验从@RequestBody来的实体,失败抛出:springframeword.MethodArgumentNotValidException
        @PostMapping("/test01")
        public String test01(@Validated @RequestBody Person person) {
            return "通过校验" + " " + person.toString();
        @ApiOperation(value = "test02", notes = "校验普通实体")
        //校验普通实体失败抛出:org.springframework.validation.BindException
        @GetMapping("/test02")
        public String test02(@Validated Person person) {
            return "通过校验" + " " + person.toString();
        @ApiOperation(value = "test03", notes = "校验普通参数")
        //校验普通参数失败抛出:javax.validation.ConstraintViolationException
        @PostMapping("/test03")
        public String test03(@RequestBody @Email String email) {
            return "通过校验" + " " + email;
    public class Person {
        @Max(value = 50)
        private Integer age;
    

    拦截处理异常

  • 由于校验不通过则立即失败,抛出异常,本次请求结束,响应500。
  • 为了不让用户对响应的500而产生疑惑,所以我们需要在全局异常捕获器中捕获Validator的异常,并响应给客户看得懂的信息。
  • 解决方案:新建一个配置类,并添加@RestControllerAdvice注解,然后在具体方法中通过 @ExceptionHandler指定需要处理的异常

    import com.hackyle.boot.common.pojo.ApiResponse;
    import org.springframework.validation.BindException;
    import org.springframework.web.HttpMediaTypeNotSupportedException;
    import org.springframework.web.HttpRequestMethodNotSupportedException;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.servlet.NoHandlerFoundException;
    import javax.naming.AuthenticationException;
    import javax.validation.ConstraintViolationException;
    @RestControllerAdvice
    public class GlobalExceptionHandler {
         * Validator校验RequestBody实体不通过抛出的异常
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ApiResponse<String> methodArgumentNotValidException(MethodArgumentNotValidException e) {
            //LOGGER.info("全局异常捕获器-捕获到MethodArgumentNotValidException:", e);
            return ApiResponse.error(9999, "校验RequestBody的实体不通过"); //这里为了代码方便,就不放于枚举类了
         * Validator校验单个参数校验失败抛出的异常
        @ExceptionHandler(ConstraintViolationException.class)
        public ApiResponse<String> constraintViolationException(ConstraintViolationException e) {
            //LOGGER.info("全局异常捕获器-捕获到ConstraintViolationException:", e);
            return ApiResponse.error(9999, "处理单个参数校验失败抛出的异常");
         * Validator校验普通实体失败抛出的异常
        @ExceptionHandler(BindException.class)
        public ApiResponse<String> bindException(BindException e) {
            //LOGGER.info("全局异常捕获器-捕获到BindException:", e);
            return ApiResponse.error(9999, "校验普通实体失败抛出的异常");
         * 请求方法不被允许异常
        @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
        public ApiResponse<String> httpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
            //LOGGER.info("全局异常捕获器-捕获到HttpRequestMethodNotSupportedException:", e);
            return ApiResponse.error(9999, "请求方法不被允许异常");
        @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
        public ApiResponse<String> httpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
            //LOGGER.info("全局异常捕获器-捕获到HttpRequestMethodNotSupportedException:", e);
            return ApiResponse.error(9999, "HTTP请求不支持");
        @ExceptionHandler(NoHandlerFoundException.class)
        public ApiResponse<String> noHandlerFoundException(NoHandlerFoundException e) {
            //LOGGER.info("全局异常捕获器-捕获到NoHandlerFoundException:", e);
            return ApiResponse.error(9999, "接口不存在");
        @ExceptionHandler(AuthenticationException.class)
        public ApiResponse<String> authenticationException(AuthenticationException e) {
            //LOGGER.info("全局异常捕获器-捕获到AuthenticationException:", e);
            return ApiResponse.error(9999, "认证异常");
         * 总异常:只要出现异常,总会被这个拦截,因为所以的异常父类为:Exception
        @ExceptionHandler(Exception.class)
        public ApiResponse<String> exception(Exception e) {
            //LOGGER.info("全局异常捕获器-捕获到Exception:", e);
            return ApiResponse.error(9999, "总异常");
    

    主要测试点

  • 校验JSON传来的实体类
  • 校验表单传来的实体类
  • 校验单个参数
  • 校验嵌套传来的JSON实体类,注意在嵌套检测处要使用“@Valid”
  • JSON实体类

    //校验从@RequestBody来的实体,失败抛出:springframeword.MethodArgumentNotValidException
    @PostMapping("/requestBody")
    public String requestBody(@Validated @RequestBody PersonAddDto person) {
        return "通过校验" + " " + JSON.toJSONString(person);
    

    表单实体类

    //校验普通实体失败抛出:org.springframework.validation.BindException
    @PostMapping("/entity")
    public String entity(@Validated PersonAddDto person) {
        return "通过校验" + " " + JSON.toJSONString(person);
    

    校验单个参数

    //校验普通参数失败抛出:javax.validation.ConstraintViolationException
    //注意:在使用属性校验参数前一定要额外加“@Validated”,也可以加在类上
    @PostMapping("/param")
    public String param(@Validated @Email @RequestParam("email") String email) {
        return "通过校验" + " " + email;
    

    数组与嵌套

    //数组与嵌套
    @PostMapping("/listNest")
    public String listNest(@Validated @RequestBody ApiRequest<List<PersonAddDto>> apiRequest) {
        return "通过校验" + " " + JSON.toJSONString(apiRequest);
    
  • 对于同一实体,在不同场景下需要校验的参数也是不同的。
  • 例如,在增加操作的时候,需要校验username、password;在删除操作的时候,需要校验ID;在修改操作的时候,也需要校验ID和某些字段;在查询操作的时候需要校验ID。
  • 创建分组校验
  • 在实体类上加各个分组标识
  • 在Controller层的Validated注解中也加入分组标识
  • 定义分组标识

    import javax.validation.groups.Default;
    public interface ValidatedGroup extends Default {
        interface Create extends ValidatedGroup {
        interface Update extends ValidatedGroup {
        interface Query extends ValidatedGroup {
        interface Delete extends ValidatedGroup {
    

    属性使用校验注解

    import com.ks.demo.vv.config.ValidatedGroup;
    import lombok.Data;
    import org.hibernate.validator.constraints.Range;
    import javax.validation.constraints.*;
    import java.math.BigDecimal;
    import java.util.Date;
    @Data
    public class PersonAddDto {
        //使用了group的,限定只有在该个标记的情况下才会使得当前校验生效
        //在Controller层使用:@Validated(ValidatedGroup.Update.class) @RequestBody PersonAddDto personAddDto
        //含义:限定在更新和删除时,必须携带ID
        @NotNull(groups = {ValidatedGroup.Update.class, ValidatedGroup.Delete.class}, message = "更新操作时不允许id为空")
        private Long id;
        @NotBlank(message = "请输入名称")
        @Size(message = "名称字符长度在{min}到{max}之间", min = 1, max = 10)
        private String name;
        @NotNull(message = "请输入年龄")
        @Range(message = "年龄范围为 {min} 到 {max} 之间", min = 1, max = 100)
        private Integer age;
    

    在Controller层指定开启那些校验

    import com.alibaba.fastjson2.JSON;
    import com.ks.demo.vv.config.ValidatedGroup;
    import com.ks.demo.vv.dto.ApiRequest;
    import com.ks.demo.vv.dto.PersonAddDto;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;
    import javax.validation.constraints.Email;
    import java.util.List;
    @RequestMapping("/validated")
    @RestController
    public class ValidatedGroupController {
         * `@Validated`中不指定任何分组
        @PostMapping("/groupAdd")
        public String groupAdd(@Validated @RequestBody ApiRequest<PersonAddDto> apiRequest) {
            return "通过校验" + " " + JSON.toJSONString(apiRequest);
         * `@Validated`中限定使用'ValidatedGroup.Update.class'分组
         * PersonAddDto实体类中的所有加了该个分组标识的校验都生效
        @PostMapping("/groupUpdate")
        public String groupUpdate(@Validated(ValidatedGroup.Update.class) @RequestBody ApiRequest<PersonAddDto> apiRequest) {
            return "通过校验" + " " + JSON.toJSONString(apiRequest);
         * `@Validated`中限定'ValidatedGroup.Delete.class'分组
         * PersonAddDto实体类中的所有加了该个分组标识的校验都生效
        @PostMapping("/groupDel")
        public String groupDel(@Validated(ValidatedGroup.Delete.class) @RequestBody ApiRequest<PersonAddDto> apiRequest) {
            return "通过校验" + " " + JSON.toJSONString(apiRequest);
                
    gorge_yong

    谢谢,解决了我的问题

    与SpringBoot整合

    在SpringBootv2.3之前的版本只需要引入 web 依赖就可以了,他包含了validation校验包
    而在此之后SpringBoot版本就独立出来了需要单独引入依赖

    Fri Oct 13 09:28:34 CST 2023