工程创建

image.png
image.png

依赖引入

在SpringBoot2.3之后Hibernate-validation需要手动引入

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

image.png
不想去写Getter和Setter

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

传统方式校验

创建实体类

package com.dance.sharevalidator.entity;

import lombok.Data;

import java.time.LocalDateTime;

/**
 * 组织机构类
 */
@Data
public class Dept {
    /**
     * Id
     */
    private Integer id;
    /**
     * 父ID
     */
    private Integer parentId;
    /**
     * 名称
     */
    private String name;
    /**
     * 成立时间
     */
    private LocalDateTime createTime;
}

创建了一个机构类,先对这个类做简单的校验

创建Controller

package com.dance.sharevalidator.controller;

import com.dance.sharevalidator.entity.Dept;
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 java.time.LocalDateTime;

@RestController
@RequestMapping("/dept")
public class DeptController {

    /**
     * http://localhost:8080/dept
     * 添加部门
     * addRoot 添加根节点 parentId = 0
     * add
     * @param dept 要添加的部门
     * @return 结果
     */
    @PostMapping
    public String add(@RequestBody Dept dept){
        /**
         * id           必须是 null
         * parentId     不能为 null, 必须大于0
         * name         不能为空(null/""), 长度必须大于0
         * createTime   不是未来的时间
         *
         * 注: 返回消息在正式开发中不建议直接返回具体的错误,而是用一些安全性的术语
         * 我这里只是个人Demo
         */
        if (dept.getId() != null) {
            return "id 必须null";
        }
        if (dept.getParentId() == null) {
            return "parentId 为 null";
        }
        if (dept.getParentId() < 0){
            return "parentId 小于 0";
        }
        if (dept.getName() == null) {
            return "机构名称 为 null";
        }
        if ("".equals(dept.getName().trim())){
            return "机构名称 为 空串";
        }
        if (dept.getCreateTime() == null){
            // 设置为当前时间
            dept.setCreateTime(LocalDateTime.now());
        }else{
            boolean isBefore = dept.getCreateTime().isBefore(LocalDateTime.now());
            if (!isBefore){
                return "成立时间不能大于当前时间";
            }
        }


        return "ok";
    }

}

采用传统方式校验

启动项目


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.6)

2022-04-12 23:26:01.368  INFO 61968 --- [           main] c.d.s.ShareValidatorApplication          : Starting ShareValidatorApplication using Java 1.8.0_202 on LAPTOP-7AG9LQEU with PID 61968 (E:\coding\share-validator\target\classes started by 86136 in E:\coding\share-validator)
2022-04-12 23:26:01.370  INFO 61968 --- [           main] c.d.s.ShareValidatorApplication          : No active profile set, falling back to 1 default profile: "default"
2022-04-12 23:26:01.797  INFO 61968 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-04-12 23:26:01.801  INFO 61968 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-04-12 23:26:01.801  INFO 61968 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.60]
2022-04-12 23:26:01.869  INFO 61968 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-04-12 23:26:01.870  INFO 61968 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 480 ms
2022-04-12 23:26:02.036  INFO 61968 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-04-12 23:26:02.042  INFO 61968 --- [           main] c.d.s.ShareValidatorApplication          : Started ShareValidatorApplication in 0.872 seconds (JVM running for 1.561)

启动OK

使用PostMan校验

打开PostMan
没有的小伙伴,自己去网上下载一个
新建一个请求
image.png
点击Send
image.png
测试ok

开始使用Validation

使用注解替换掉代码判断

package com.dance.sharevalidator.entity;

import lombok.Data;

import javax.validation.constraints.*;
import java.time.LocalDateTime;

/**
 * 组织机构类
 */
@Data
public class Dept {

    /**
     * Id
     * 使用 @Null 校验必须是null
     */
    @Null
    private Integer id;
    /**
     * 父ID
     * 使用 @NotNull 校验非null
     * 使用 @Min 校验必须大于等于0
     */
    @NotNull
    @Min(value = 0)
    private Integer parentId;
    /**
     * 名称
     * 使用 @NotBlank 校验非空(null/空串)
     */
    @NotBlank
    private String name;
    /**
     * 成立时间
     * 使用 @NotNull 校验非null
     * 使用 @PastOrPresent 校验日期必须是当前时间或者之前, 不能是未来时间
     */
    @NotNull
    @PastOrPresent
    private LocalDateTime createTime;

}
package com.dance.sharevalidator.controller;

import com.dance.sharevalidator.entity.Dept;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/dept")
@Validated // <---- 通过这个注解让validation验证这个类
public class DeptController {

    /**
     * http://localhost:8080/dept
     * 添加部门
     * addRoot 添加根节点 parentId = 0
     * add
     *
     * @param dept 要添加的部门
     * @return 结果
     * @Valid <-----使用这个注解让 这个注解后面的参数被校验
     */
    @PostMapping
    public String add(@RequestBody @Valid Dept dept) {
        // 终于可以开始写业务了
        return "ok";
    }

}

重启服务器

重启

使用PostMan进行校验

image.png
但是我这个却没有错误提示

异常消息回显问题

2022-04-13 00:00:32.053  WARN 52652 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.dance.sharevalidator.controller.DeptController.add(com.dance.sharevalidator.entity.Dept) with 4 errors: [Field error in object 'dept' on field 'name': rejected value [null]; codes [NotBlank.dept.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dept.name,name]; arguments []; default message [name]]; default message [不能为空]] [Field error in object 'dept' on field 'parentId': rejected value [null]; codes [NotNull.dept.parentId,NotNull.parentId,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dept.parentId,parentId]; arguments []; default message [parentId]]; default message [不能为null]] [Field error in object 'dept' on field 'id': rejected value [2]; codes [Null.dept.id,Null.id,Null.java.lang.Integer,Null]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dept.id,id]; arguments []; default message [id]]; default message [必须为null]] [Field error in object 'dept' on field 'createTime': rejected value [null]; codes [NotNull.dept.createTime,NotNull.createTime,NotNull.java.time.LocalDateTime,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [dept.createTime,createTime]; arguments []; default message [createTime]]; default message [不能为null]] ]

看着是被一个DefaultHandlerExceptionResolver异常解析器给拦截了,我丢
这种情况可以采用自定义全局异常来处理,但是比较麻烦, 我看网上说把SpringBoot版本切换回2.3.3以前即可

切换版本

修改POM.xml为2.1.9.RELEASE

<version>2.1.9.RELEASE</version>

同时去除POM.xml中的validation-starter,因为2.1.9中SpringBootWeb自带
image.png

重启项目

重启

使用PostMan测试

返回结果携带了异常消息

{
    "timestamp": "2022-04-12T16:29:26.778+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Null.dept.id",
                "Null.id",
                "Null.java.lang.Integer",
                "Null"
            ],
            "arguments": [
                {
                    "codes": [
                        "dept.id",
                        "id"
                    ],
                    "arguments": null,
                    "defaultMessage": "id",
                    "code": "id"
                }
            ],
            "defaultMessage": "必须为null",
            "objectName": "dept",
            "field": "id",
            "rejectedValue": 2,
            "bindingFailure": false,
            "code": "Null"
        },
        {
            "codes": [
                "NotBlank.dept.name",
                "NotBlank.name",
                "NotBlank.java.lang.String",
                "NotBlank"
            ],
            "arguments": [
                {
                    "codes": [
                        "dept.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "不能为空",
            "objectName": "dept",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotBlank"
        },
        {
            "codes": [
                "NotNull.dept.createTime",
                "NotNull.createTime",
                "NotNull.java.time.LocalDateTime",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "dept.createTime",
                        "createTime"
                    ],
                    "arguments": null,
                    "defaultMessage": "createTime",
                    "code": "createTime"
                }
            ],
            "defaultMessage": "不能为null",
            "objectName": "dept",
            "field": "createTime",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "NotNull.dept.parentId",
                "NotNull.parentId",
                "NotNull.java.lang.Integer",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "dept.parentId",
                        "parentId"
                    ],
                    "arguments": null,
                    "defaultMessage": "parentId",
                    "code": "parentId"
                }
            ],
            "defaultMessage": "不能为null",
            "objectName": "dept",
            "field": "parentId",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        }
    ],
    "message": "Validation failed for object='dept'. Error count: 4",
    "path": "/dept"
}

使用正确数据测试

image.png
OK了
我看后台结果也是被DefaultHandlerExceptionResolver拦截了呀
image.png
应该是在后续的版本中有修改了

自定义提示信息

/**
 * Id
 * 使用 @Null 校验必须是null
 */
@Null(message = "主键ID只能为Null")
private Integer id;

这样就可以了, 每个校验注解都有message属性可以设置

Validation自带注解总结

注解 可用于的数据类型 说明
Null 所有类型 验证元素值必须为 null
NotNull 所有类型 验证元素值必须不为 null
NotBlank CharSequence 验证元素值不能为 null,并且至少包含一个非空白字符。
NotEmpty CharSequence、Collection、Map、Array 验证元素值不能为 null,且不能为空
Size(min = min, max = max) 同 NotEmpty 验证元素的 size 必须在 min 和 max 之间(包含边界),认为 null 是有效的
AssertFalse boolean、Boolean 验证元素值必须为 false,认为 null 是有效的
AssertTrue 同 AssertFalse 验证元素值必须为 true,认为 null 是有效的
DecimalMax(value=, inclusive=) BigDecimal、BigInteger、CharSequence,byte、 short、int、long 及其包装类型,由于舍入错误,不支持double和float 验证元素值必须小于等于指定的 value 值,认为 null 是有效的
DecimalMin 同 DecimalMax 验证元素值必须大于等于指定的 value 值,认为 null 是有效的
Max 同 DecimalMax,不支持CharSequence 验证元素值必须小于等于指定的 value 值,认为 null 是有效的
Min 同 DecimalMax,不支持CharSequence 验证元素值必须大于等于指定的 value 值,认为 null 是有效的
Digits(integer =, fraction =) 同 DecimalMax 验证元素整数位数的上限 integer 与小数位数的上限 fraction,认为 null 是有效的
Positive BigDecimal、BigInteger,byte、short、int、long、float、double 及其包装类型 验证元素必须为正数,认为 null 是有效的
PositiveOrZero 同Positive 验证元素必须为正数或 0,认为 null 是有效的
Negative 同Positive 验证元素必须为负数,认为 null 是有效的
NegativeOrZero 同Positive 验证元素必须为负数或 0,认为 null 是有效的
Future Date、Calendar、Instant、LocalDate、LocalDateTime、LocalTime、MonthDay、OffsetDateTime、OffsetTime、Year、YearMonth、ZonedDateTime、HijrahDate、JapaneseDate、MinguoDate、ThaiBuddhistDate 验证元素值必须是一个将来的时间,认为 null 是有效的
FutureOrPresent 同 Future 验证元素值必须是当前时间或一个将来的时间,认为 null 是有效的
Past 同 Future 验证元素值必须是一个过去的时间,认为 null 是有效的
PastOrPresent 同 Future 验证元素值必须是当前时间或一个过去的时间,认为 null 是有效的
Email(regexp = 正则表达式,flag = 标志的模式) CharSequence 验证注解的元素值是Email,可以通过 regexp 和 flag 指定自定义的 email 格式,认为 null 是有效的
Pattern 同 Email 验证元素值符合正则表达式,认为 null 是有效的

可以不定义在实体类中,可以直接用于Controller方法的直接参数上

验证错误异常处理

定义统一返回对象

package com.dance.sharevalidator.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

@Data
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public class ResultVo<T> {

    /**
     * 是否成功
     */
    private Boolean success;

    /**
     * 状态码
     */
    private String code;

    /**
     * 消息
     */
    private String msg;

    /**
     * 数据
     */
    private T data;

    public ResultVo() {
    }

    public ResultVo(Boolean success) {
        this.success = success;
    }

    public ResultVo(Boolean success, String code) {
        this(success);
        this.code = code;
    }

    public ResultVo(Boolean success, String code, String msg) {
        this(success, code);
        this.msg = msg;
    }

    public ResultVo(Boolean success, String code, String msg, T data) {
        this(success,code,msg);
        this.data = data;
    }

    public static ResultVo<Object> success() {
        return new ResultVo<>(true);
    }

    public static <T> ResultVo<T> success(T data) {
        return new ResultVo<>(true, "","", data);
    }

    public static ResultVo<Object> error() {
        return new ResultVo<>(false, "500");
    }

    public static ResultVo<Object> error(String msg) {
        return new ResultVo<>(false, "500", msg);
    }

    public static <T> ResultVo<Object> error(String msg, T data) {
        return new ResultVo<>(false, "500", msg, data);
    }

}

改造Controller

@PostMapping
public ResultVo<Object> add(@RequestBody @Valid Dept dept) {
    // 终于可以开始写业务了
    return ResultVo.success();
}

启动项目错误验证

{
    "timestamp": "2022-04-13T14:23:23.553+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Null.dept.id",
                "Null.id",
                "Null.java.lang.Integer",
                "Null"
            ],
            "arguments": [
                {
                    "codes": [
                        "dept.id",
                        "id"
                    ],
                    "arguments": null,
                    "defaultMessage": "id",
                    "code": "id"
                }
            ],
            "defaultMessage": "主键ID只能为Null",
            "objectName": "dept",
            "field": "id",
            "rejectedValue": 2,
            "bindingFailure": false,
            "code": "Null"
        },
       ...............
    "message": "Validation failed for object='dept'. Error count: 4",
    "path": "/dept"
}

返回这么一大堆错误, 显然不太友好, 其实真正有用的其实就是 code字段是id,消息描述是defaultMessage
image.png
在调用时是报了一个这样的错误方法参数验证不通过异常, 自定义拦截异常, 重点这里也解决了上面留下坑,就是切版本的之前的那个问题

Controller新增异常拦截方法

@ExceptionHandler
public ResultVo<Object> exceptionHandler(MethodArgumentNotValidException exception){
    return ResultVo.error("参数错误");
}

重启项目校验

image.png
ok,返回结果为自定义的异常, 但是这里有个小问题, data:null 其实这个是不需要的

JSON非空才序列化

在ResultVo上添加

@JsonInclude(value = JsonInclude.Include.NON_NULL)

这样的话, 为null, 就不会序列化

重启项目校验

image.png
ok,为空的data字段也没有了, 但是现在的提示并不友好, 只知道是参数错误, 不知道是什么参数错误, 改造异常返回结果方法

改造异常处理方法,并将异常结果回填前端

@ExceptionHandler
public ResultVo<Object> exceptionHandler(MethodArgumentNotValidException exception){
    Map<String, String> errorMap = exception.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
    return ResultVo.error("参数错误",errorMap);
}

重启项目校验

image.png
可以看到, 已经提取出所有的错误了
image.png
这样的异常结果,显然比较友好, 但是现在这个异常处理方法是写在当前Controller中的只能对当前Controller生效,接下来我们将它抽取成全局的

抽取为全局异常处理器

package com.dance.sharevalidator.config;

import com.dance.sharevalidator.vo.ResultVo;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;
import java.util.stream.Collectors;

@RestControllerAdvice
public class HandlerException {

    @ExceptionHandler
    public ResultVo<Object> exceptionHandler(MethodArgumentNotValidException exception){
        Map<String, String> errorMap = exception.getBindingResult()
                .getFieldErrors()
                .stream()
                .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
        return ResultVo.error("参数错误",errorMap);
    }
}

这样的话其他Controller也会生效

重启项目校验

校验结果一致,没有问题

级联验证

创建一对一级联验证环境

创建Emp类

package com.dance.sharevalidator.entity;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Null;

@Data
public class Emp {

    /**
     * id
     */
    @Null
    private Integer id;

    /**
     * 名称
     */
    @NotEmpty
    private String name;

    /**
     * 一个员工属于一个部门(不要强行说多部门容易挨揍)
     */
    private Dept dept;
}

新建Controller

package com.dance.sharevalidator.controller;

import com.dance.sharevalidator.entity.Emp;
import com.dance.sharevalidator.vo.ResultVo;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/emp")
@Validated
public class EmpController {

    @PostMapping
    public ResultVo<Object> add(@RequestBody @Valid Emp emp){
        return ResultVo.success();
    }

}

重启项目校验

image.png
ok,自身校验生效
image.png
但是存在一个问题, 属性Dept部门却没有校验,到此环境OK

一对一级联验证

在Emp类的dept字段上添加注解

@Valid
private Dept dept;

重启项目校验

image.png
这个时候就会将dept对象级联验证掉

创建一对多级联验证环境

Dept类新增字段

/**
 * 一个部门多个人员
 */
private List<Emp> empList;

重启项目校验

调用之前的接口dept
image.png
显然它并没有校验部门中的人员empList,到此环境OK

一对多级联验证

在empList字段上添加注解

/**
 * 一个部门多个人员
 */
@Valid
private List<Emp> empList;

重启项目校验

image.png
这个时候,一对多也顺带校验掉了, 但是这么写 像是在校验集合,而不是里面的Emp, 所以大佬推荐了更好的写法,当然这个是个人习惯, 但是却让我眼前一亮, 发现居然可以在泛型里面写注解[牛]

注解改进

/**
 * 一个部门多个人员
 */
private List<@Valid Emp> empList;

重启项目校验

效果一样.okk

在Service层做参数验证

为什么要在Service层做参数验证

  • 因为有的Service不只是提供给Controller调用,还提供给RPC调用, 在Service层做的话,就避免了在Controller和RPC层都做参数验证
  • 只提供给Controller使用, 但是Controller中, 有调用其他的第三方接口, 或者RPC服务, 所以统一在Service层做验证

    模拟两种情况

    携带接口

    新建Service接口

    ```java package com.dance.sharevalidator.service;

import com.dance.sharevalidator.entity.Emp;

public interface IEmpService { void add(Emp emp); }

<a name="HzemO"></a>
#### 新建Service实现
```java
package com.dance.sharevalidator.service.impl;

import com.dance.sharevalidator.entity.Emp;
import com.dance.sharevalidator.service.IEmpService;
import org.springframework.stereotype.Service;

@Service
public class EmpService implements IEmpService {
    @Override
    public void add(Emp emp) {
        // 写业务逻辑
        System.out.println("员工添加成功");
    }
}

不携带接口

package com.dance.sharevalidator.service;

import com.dance.sharevalidator.entity.Dept;
import org.springframework.stereotype.Service;

@Service
public class DeptService {
    public void add(Dept dept){
        System.out.println("部门添加成功");
    }
}

工作中如果一个Service有多个实现的话, 才写接口和实现类两层,当然如果是RPC除外,RPC是需要提供接口给外部用于交互的, 如果只是内部调用的话, 只有一个实现类,完全不需要写接口层, 当然具体还是得看项目, 应为依赖倒转原则提议面向高层开发,而不是面向细节, 但是还是要具体自行衡量

不携带接口Service层验证

将方法上的注解,和类上的注解移动到Service层中

package com.dance.sharevalidator.service;

import com.dance.sharevalidator.entity.Dept;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;

@Service
@Validated
public class DeptService {
    public void add(@Valid Dept dept){
        System.out.println("部门添加成功");
    }
}

将Controller层上的去掉, 并且添加调用逻辑

.....
@RestController
@RequestMapping("/dept")
//@Validated // <---- 通过这个注解让validation验证这个类
public class DeptController {

    @Autowired
    private DeptService deptService;

    .....
    @PostMapping
    public ResultVo<Object> add(@RequestBody /*@Valid*/ Dept dept) {
        // 终于可以开始写业务了
        deptService.add(dept);
        return ResultVo.success();
    }

}

重启项目校验

image.png
突然变成了这样

javax.validation.ConstraintViolationException: add.dept.parentId: 不能为null, add.dept.id: 主键ID只能为Null, add.dept.name: 不能为空, add.dept.createTime: 不能为null

后台发现是报错异常变了,变成了ConstraintViolationException

编写Service层的验证异常处理

HandlerException中添加处理这个异常的方法

@ExceptionHandler
public ResultVo<Object> exceptionHandler(ConstraintViolationException exception) {
    Map<Path, String> errorMap = exception.getConstraintViolations()
            .stream()
            .collect(Collectors.toMap(ConstraintViolation::getPropertyPath, ConstraintViolation::getMessage));
    return ResultVo.error("参数错误", errorMap);
}

重启项目校验

image.png
ok

IDEA Debug Evaluete功能

我也是看UP主用这个的,也是IDEA的调试功能,感觉非常的好用,在这里推荐一下大家
在调试的时候可以直接这里写表达式,来准确的获取层级结构和数据,或者快速定位调试参数
image.png
我这里想收集异常结果,可以直接在里面写代码,然后执行,获取结果,等调试完成直接将代码粘贴到方法中就可以了
image.png

携带接口Service层验证

将Controller层的注解移动到Service层
Controller改造

.....
@RestController
@RequestMapping("/emp")
//@Validated
public class EmpController {

    @Autowired
    private IEmpService empService;

    @PostMapping
    public ResultVo<Object> add(@RequestBody /*@Valid*/ Emp emp){
        empService.add(emp);
        return ResultVo.success();
    }

}

Service实现类改造

....
@Service
@Validated
public class EmpService implements IEmpService {
    @Override
    public void add(@Valid Emp emp) {
        // 写业务逻辑
        System.out.println("员工添加成功");
    }
}

重启项目校验

image.png
显然和理想的一样, 报错了

javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method EmpService#add(Emp) redefines the configuration of IEmpService#add(Emp).

这个错误提示你的注解应该是写在接口上的, 而不是实现类

将注解移动到接口

实现类去掉注解

....
@Service
//@Validated
public class EmpService implements IEmpService {
    @Override
    public void add(/*@Valid*/ Emp emp) {
        // 写业务逻辑
        System.out.println("员工添加成功");
    }
}

接口增加注解

package com.dance.sharevalidator.service;

import com.dance.sharevalidator.entity.Emp;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;

@Validated
public interface IEmpService {
    void add(@Valid Emp emp);
}

重启项目校验

image.png
本来我以为是都移动到接口上的, 但是没有想到我还是考虑的太少了, 这里的@Validated 应该是放在实现类上的, 应为如果一个接口有好多实现类, 别的实现类不想走参数验证, 那么这不就歇菜了, 所以将@Validated这个注解移动回实现类上,这样想走验证的实现类就自己加, 不想走的, 就可以不加

注解回移

实现类改造

package com.dance.sharevalidator.service.impl;

import com.dance.sharevalidator.entity.Emp;
import com.dance.sharevalidator.service.IEmpService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;

@Service
@Validated
public class EmpService implements IEmpService {
    @Override
    public void add(/*@Valid*/ Emp emp) {
        // 写业务逻辑
        System.out.println("员工添加成功");
    }
}

接口改造

package com.dance.sharevalidator.service;

import com.dance.sharevalidator.entity.Emp;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;

public interface IEmpService {
    void add(@Valid Emp emp);
}

重启项目校验

image.png
这样就比较好了

扩展

注解不一定非要加在实体类上,也可以加在入参,出参 等地方

@NotNull Emp getById(@NotNull Long id);

这样也是可以的, 并且不需要加@Valid

分组验证

需求

在新增的时候ID不能有值, 但是在修改的时候, ID必须有值, 这个时候就需要用到分组验证了

恢复环境

将EmpService中的验证注解给注释掉
image.png
image.png
Controller中恢复验证
image.png
增加更新方法

增加分组校验

修改实体类

package com.dance.sharevalidator.entity;

import lombok.Data;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;

@Data
public class Emp {

    /**
     * id
     * 希望在添加的时候 @Null生效
     * 希望在更新的时候 @NotNull生效
     * groups 如果是一个可以不写大括号, 多个必须写
     */
    @Null(groups = {Add.class})
    @NotNull(groups = Update.class)
    private Integer id;

    public interface Add{}
    public interface Update{}

    /**
     * 名称
     */
    @NotEmpty
    private String name;

    /**
     * 一个员工属于一个部门(不要强行说多部门容易挨揍)
     */
    @Valid
    private Dept dept;
}

修改Controller

image.png
将@Valid 修改为@Validated 并传入分组参数

测试

image.png
image.png
可以看到, 只有ID校验生效了, 其他的校验没有生效, 为什么呢?
应为

  • 有分组的校验,只属于该分组
  • 没有分组的校验,属于默认组

修复问题
image.png
添加默认分组, 类为javax.validation.groups.Default这个

重启测试

image.png
其他校验也生效了,ok

自定义注解验证

模拟场景

新增岗位类

package com.dance.sharevalidator.entity;

import lombok.Data;

import java.util.List;

/**
 * 岗位类
 */
@Data
public class Job {

    private Integer id;

    private String name;

    private List<String> labels;

}

新增控制器

package com.dance.sharevalidator.controller;

import com.dance.sharevalidator.entity.Job;
import com.dance.sharevalidator.vo.ResultVo;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/job")
@Validated
public class JobController {

    @PostMapping
    public ResultVo<String> add(@RequestBody @Valid Job job){
        return ResultVo.success("ok");
    }

}

需求

强行提一个需求

  1. 对于Integer而言, 必须是3的倍数
  2. 对于List而言, 元素个数必须是3的倍数

不要在意细节, 关注技术

实现

新增自定义注解

package com.dance.sharevalidator.validation;

import com.dance.sharevalidator.validation.validator.MultipleOfThreeForCollection;
import com.dance.sharevalidator.validation.validator.MultipleOfThreeForInteger;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {MultipleOfThreeForInteger.class, MultipleOfThreeForCollection.class}
)
public @interface MultipleOfThree {
    String message() default "必须是3的倍数";

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

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

新增校验

package com.dance.sharevalidator.validation.validator;

import com.dance.sharevalidator.validation.MultipleOfThree;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * 针对Integer类型的验证
 */
public class MultipleOfThreeForInteger implements ConstraintValidator<MultipleOfThree, Integer> {
    @Override
    public void initialize(MultipleOfThree constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        if (integer == null) {
            return true;
        }
        return integer % 3 == 0;
    }
}
package com.dance.sharevalidator.validation.validator;

import com.dance.sharevalidator.validation.MultipleOfThree;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Collection;

/**
 * 针对Collection类型的验证
 */
public class MultipleOfThreeForCollection implements ConstraintValidator<MultipleOfThree, Collection> {

    @Override
    public void initialize(MultipleOfThree constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    @Override
    public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {
        if (null == collection){
            return true;
        }
        return collection.size() % 3 ==0;
    }
}

实体类添加

package com.dance.sharevalidator.entity;

import com.dance.sharevalidator.validation.MultipleOfThree;
import lombok.Data;

import java.util.List;

/**
 * 岗位类
 */
@Data
public class Job {

    @MultipleOfThree
    private Integer id;

    private String name;

    @MultipleOfThree
    private List<String> labels;

}

测试验证

image.png
自定义注解ok

List中做分组校验

模拟场景

新增方法

empController 中新增批量方法

@PostMapping("/addList")
public ResultVo<Object> addList(@RequestBody @Validated({Emp.Add.class, Default.class}) List<Emp> empList) {
    return ResultVo.success();
}

测试

image.png
测试发现校验没有生效,场景ok

实现

新增注解

package com.dance.sharevalidator.validation;

import com.dance.sharevalidator.validation.validator.MultipleOfThreeForCollection;
import com.dance.sharevalidator.validation.validator.MultipleOfThreeForInteger;
import com.dance.sharevalidator.validation.validator.ValidListValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.groups.Default;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
        validatedBy = {ValidListValidator.class}
)
public @interface ValidList {
    String message() default "";

    boolean quickFail() default false;

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

    Class<?>[] groupings() default {Default.class};

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

新增工具类

package com.dance.sharevalidator.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.validation.Validator;

@Component
public class ValidatorUtils {

    public static Validator validator;

    @Autowired
    public void setValidator(Validator validator) {
        ValidatorUtils.validator = validator;
    }
}

新增注解实现

package com.dance.sharevalidator.validation.validator;

import com.dance.sharevalidator.config.ValidatorUtils;
import com.dance.sharevalidator.exception.ValidListException;
import com.dance.sharevalidator.validation.ValidList;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import java.util.*;

public class ValidListValidator implements ConstraintValidator<ValidList, List> {
    Class<?>[] groupings;

    boolean quickFail;

    @Override
    public void initialize(ValidList constraintAnnotation) {
        groupings = constraintAnnotation.groupings();
        quickFail = constraintAnnotation.quickFail();
    }

    @Override
    public boolean isValid(List collection, ConstraintValidatorContext constraintValidatorContext) {
        if (collection == null) {
            return true;
        }
        Map<Integer, Set<ConstraintViolation<Object>>> errors = new HashMap<>();
        for (int i = 0; i < collection.size(); i++) {
            Object o = collection.get(i);
            // 有几个错误, 就有几个元素
            Set<ConstraintViolation<Object>> validate = ValidatorUtils.validator.validate(o, groupings);
            if (null != validate && 0 != validate.size()) {
                errors.put(i, validate);
                // 快速失败
                if (quickFail) {
                    break;
                }
            }
        }
        if (errors.size() > 0) {
            // 通过抛异常的手段来 让我们自己的全局异常拦截器拦截, 绕过Validation的默认报错
            throw new ValidListException(errors);
        }
        return true;
    }
}

新增自定义异常

package com.dance.sharevalidator.exception;

import lombok.Getter;
import lombok.Setter;

import javax.validation.ConstraintViolation;
import java.util.Map;
import java.util.Set;

@Getter
@Setter
public class ValidListException extends RuntimeException{
    private Map<Integer, Set<ConstraintViolation<Object>>> errors;

    public ValidListException(Map<Integer, Set<ConstraintViolation<Object>>> errors) {
        this.errors = errors;
    }
}

新增异常捕获

@ExceptionHandler
public ResultVo<Object> exceptionHandler(ValidationException exception) {
    Throwable cause = exception.getCause();
    if (cause instanceof ValidListException) {
        Map<Integer, Map<Path, String>> errorMap = new HashMap<>();
        ValidListException validListException = (ValidListException)cause;
        validListException.getErrors().forEach((i, c) -> {
            Map<Path, String> value = c.stream().collect(Collectors.toMap(ConstraintViolation::getPropertyPath, ConstraintViolation::getMessage));
            errorMap.put(i, value);
        });
        return ResultVo.error("参数错误", errorMap);
    }
    return ResultVo.error("系统异常");
}

修改EmpController方法

@PostMapping("/addList")
public ResultVo<Object> addList(@RequestBody @ValidList( groupings = {Emp.Add.class, Default.class}, quickFail = true) List<Emp> empList) {
    return ResultVo.success();
}

使用自定义注解

测试

image.png
测试ok

Bean参数间的逻辑校验

需求

强行提需求

  • 员工年龄在20-25之间 title必须以 “初级” 开头
  • 员工年龄在25-30之间 title必须以 “中级” 开头
  • .否则不做验证

    实现

    Emp新增属性

    ```java @NotNull private Integer age;

@NotEmpty @Pattern(regexp = “^初级.“, groups = TitleJunior.class) @Pattern(regexp = “^中级.“, groups = TitleMiddle.class) private String title;

// 初级 public interface TitleJunior{} // 中级 public interface TitleMiddle{}

<a name="wTtgs"></a>
### 新增SequenceProvider
```java
package com.dance.sharevalidator.validation.sequenceprovider;

import com.dance.sharevalidator.entity.Emp;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;

import java.util.ArrayList;
import java.util.List;

public class EmpGroupSequenceProvider implements DefaultGroupSequenceProvider<Emp> {
    @Override
    public List<Class<?>> getValidationGroups(Emp emp) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(Emp.class);
        if (emp != null) {
            if (emp.getAge() >= 20 && emp.getAge() < 25) {
                defaultGroupSequence.add(Emp.TitleJunior.class);
            }else if(emp.getAge() >= 25 && emp.getAge() < 30){
                defaultGroupSequence.add(Emp.TitleMiddle.class);
            }
        }
        return defaultGroupSequence;
    }
}

Emp新增注解

image.png

EmpController新增方法

@PutMapping("/update")
public ResultVo<Object> update2(@RequestBody @Valid Emp emp) {
    return ResultVo.success();
}

测试

image.png
image.png
okk, 到这里就完成了, 撒花花, 大家快去项目里面用起来吧, 让你的代码变简洁