工程创建
依赖引入
在SpringBoot2.3之后Hibernate-validation需要手动引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
不想去写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)
使用PostMan校验
打开PostMan
没有的小伙伴,自己去网上下载一个
新建一个请求
点击Send
测试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进行校验
异常消息回显问题
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自带
重启项目
使用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"
}
使用正确数据测试
OK了
我看后台结果也是被DefaultHandlerExceptionResolver拦截了呀
应该是在后续的版本中有修改了
自定义提示信息
/**
* Id
* 使用 @Null 校验必须是null
*/
@Null(message = "主键ID只能为Null")
private Integer id;
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
在调用时是报了一个这样的错误方法参数验证不通过异常, 自定义拦截异常, 重点这里也解决了上面留下坑,就是切版本的之前的那个问题
Controller新增异常拦截方法
@ExceptionHandler
public ResultVo<Object> exceptionHandler(MethodArgumentNotValidException exception){
return ResultVo.error("参数错误");
}
重启项目校验
ok,返回结果为自定义的异常, 但是这里有个小问题, data:null 其实这个是不需要的
JSON非空才序列化
在ResultVo上添加
@JsonInclude(value = JsonInclude.Include.NON_NULL)
重启项目校验
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);
}
重启项目校验
可以看到, 已经提取出所有的错误了
这样的异常结果,显然比较友好, 但是现在这个异常处理方法是写在当前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);
}
}
重启项目校验
级联验证
创建一对一级联验证环境
创建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();
}
}
重启项目校验
ok,自身校验生效
但是存在一个问题, 属性Dept部门却没有校验,到此环境OK
一对一级联验证
在Emp类的dept字段上添加注解
@Valid
private Dept dept;
重启项目校验
创建一对多级联验证环境
Dept类新增字段
/**
* 一个部门多个人员
*/
private List<Emp> empList;
重启项目校验
调用之前的接口dept
显然它并没有校验部门中的人员empList,到此环境OK
一对多级联验证
在empList字段上添加注解
/**
* 一个部门多个人员
*/
@Valid
private List<Emp> empList;
重启项目校验
这个时候,一对多也顺带校验掉了, 但是这么写 像是在校验集合,而不是里面的Emp, 所以大佬推荐了更好的写法,当然这个是个人习惯, 但是却让我眼前一亮, 发现居然可以在泛型里面写注解[牛]
注解改进
/**
* 一个部门多个人员
*/
private List<@Valid Emp> empList;
重启项目校验
在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();
}
}
重启项目校验
突然变成了这样
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);
}
重启项目校验
IDEA Debug Evaluete功能
我也是看UP主用这个的,也是IDEA的调试功能,感觉非常的好用,在这里推荐一下大家
在调试的时候可以直接这里写表达式,来准确的获取层级结构和数据,或者快速定位调试参数
我这里想收集异常结果,可以直接在里面写代码,然后执行,获取结果,等调试完成直接将代码粘贴到方法中就可以了
携带接口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("员工添加成功");
}
}
重启项目校验
显然和理想的一样, 报错了
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);
}
重启项目校验
本来我以为是都移动到接口上的, 但是没有想到我还是考虑的太少了, 这里的@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);
}
重启项目校验
扩展
注解不一定非要加在实体类上,也可以加在入参,出参 等地方
@NotNull Emp getById(@NotNull Long id);
分组验证
需求
在新增的时候ID不能有值, 但是在修改的时候, ID必须有值, 这个时候就需要用到分组验证了
恢复环境
将EmpService中的验证注解给注释掉
Controller中恢复验证
增加更新方法
增加分组校验
修改实体类
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
测试
可以看到, 只有ID校验生效了, 其他的校验没有生效, 为什么呢?
应为
- 有分组的校验,只属于该分组
- 没有分组的校验,属于默认组
修复问题
添加默认分组, 类为javax.validation.groups.Default这个
重启测试
自定义注解验证
模拟场景
新增岗位类
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");
}
}
需求
强行提一个需求
- 对于Integer而言, 必须是3的倍数
- 对于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;
}
测试验证
List中做分组校验
模拟场景
新增方法
empController 中新增批量方法
@PostMapping("/addList")
public ResultVo<Object> addList(@RequestBody @Validated({Emp.Add.class, Default.class}) List<Emp> empList) {
return ResultVo.success();
}
测试
实现
新增注解
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();
}
测试
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新增注解
EmpController新增方法
@PutMapping("/update")
public ResultVo<Object> update2(@RequestBody @Valid Emp emp) {
return ResultVo.success();
}
测试
okk, 到这里就完成了, 撒花花, 大家快去项目里面用起来吧, 让你的代码变简洁