SpringBoot-Validation优雅实现参数校验
1、是什么?
它简化了 Java Bean Validation 的集成。Java Bean Validation 通过 JSR 380,也称为 Bean Validation 2.0,是一种标准化的方式,用于在 Java 应用程序中对对象的约束进行声明式验证。它允许开发人员使用注解来定义验证规则,并自动将规则应用于相应的字段或方法参数
为了我们方便地使用参数校验功能了
2、怎么玩?
(1) 首先导入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ly</groupId>
<artifactId>springboot-validate</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
(2) 在对应实体上加@Validated注解,开启校验规则
package com.ly.valid.controller;
import com.ly.valid.common.R;
import com.ly.valid.common.ResultCodeEnum;
import com.ly.valid.entity.Person;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
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.List;
import java.util.Map;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:01
* @tags 喜欢就去努力的争取
*/
@RestController
public class TestController {
/**
* 保存
*/
@PostMapping("/test1")
public R save(@Validated @RequestBody Person person, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
Map<String, String> map = new HashMap<>(fieldErrorList.size());
fieldErrorList.forEach(item -> {
String message = item.getDefaultMessage(); // 如果没有在相应的注解中加message,则会获取默认信息 例如:@NotBlank(message = "品牌名必须提交"),获取的message则为品牌名必须提交
String field = item.getField(); // 获取哪个字段出现的问题
map.put(field, message);
});
return R.fail(ResultCodeEnum.PARAM_ERROR, map);
} else {
// 伪代码
// personService.save(person);
}
return R.success();
}
/**
* 全局处理
*
* @param person
* @return
*/
@PostMapping("/test2")
public R test(@Validated Person person) {
return R.success(person);
}
}
(3) 给对应的实体Bean添加校验注解,并自定义message提示
package com.ly.valid.entity;
import com.ly.valid.constant.RegularConstant;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:03
* @tags 喜欢就去努力的争取
*/
@Data
public class Person {
@NotBlank(message = "name 姓名不能为空")
private String name;
@NotNull(message = "age 年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
private Integer age;
@NotNull(message = "gender 性别不能为空")
private Integer gender;
@Email(regexp = RegularConstant.EMAIL, message = "email 邮箱格式不正确")
private String email;
@Pattern(regexp = RegularConstant.PHONE, message = "phone 手机号格式不正确")
private String phone;
@Past(message = "birthday 生日日期有误")
private LocalDate birthday;
}
(4) 不加@Validate注解,测试一下
(5) 开启校验功能@Validated:不使用自定义message会有默认提示信息
(6) 方式一: 我想自定义提示信息怎么整?简单,只需要给响应的校验bean后面紧跟着添加一个BindingResult,就可以获取到校验的结果了
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
Map<String, String> map = new HashMap<>();
fieldErrorList.forEach(item -> {
String message = item.getDefaultMessage(); // 如果没有在相应的注解中加message,则会获取默认信息 例如:@NotBlank(message = "品牌名必须提交"),获取的message则为品牌名必须提交
String field = item.getField(); // 获取哪个字段出现的问题
map.put(field, message);
});
return R.error(400, "提交的数据不合法").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}
(7) 测试一下
(8) 方式二:如果有很多需要这样手动一个个处理,就显得很麻烦了;所以我们需要一个全局处理
package com.ly.valid.common;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
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.List;
import java.util.stream.Collectors;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 22:13
* @tags 喜欢就去努力的争取
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理 form data方式调用接口校验失败抛出的异常 (对象参数)
*/
@ExceptionHandler(BindException.class)
public R error(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> errorMessages = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
}
/**
* 处理 json 请求体调用接口校验失败抛出的异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R error(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> errorMessages = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
}
/**
* 单个参数校验失败抛出的异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public R error(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessageTemplate)
.collect(Collectors.joining());
return R.fail(errorMsg);
}
}
(9) 测试一下
3、相关注解信息
注解 | 数据类型 | 说明 |
---|---|---|
@NotBlank | CharSequence | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@NotEmpty | CharSequence,Collection,Map,Arrays | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@Length(min=下限, max=上限) | CharSequence | 验证注解的元素值长度在min和max区间内 |
@NotNull | 所有类型 | 验证注解的元素值不是null |
@Null | 所有类型 | 验证注解的元素值是null |
@Max(value=n) | BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估字符序列表示的数字值),Number的任何子类型。 | 验证注解的元素值小于等于@Max指定的value值 |
@Min(value=n) | BigDecimal,BigInteger,byte,short,int,long和原始类型的相应包装。HV额外支持:CharSequence的任何子类型(评估char序列表示的数值),Number的任何子类型。 | 验证注解的元素值大于等于@Min指定的value值 |
@Size(min=最小值, max=最大值) | 字符串,集合,映射和数组。HV额外支持:CharSequence的任何子类型。 | 验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小 |
CharSequence | 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 | |
@Pattern(regex=正则表达式, flag=) | CharSequence | 验证注解的元素值与指定的正则表达式匹配 |
@Range(min=最小值, max=最大值 | CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始类型各自的包装 | 验证注解的元素值在最小值和最大值之间 |
@AssertFalse | Boolean, boolean | 验证注解的元素值是false |
@AssertTrue | Boolean, boolean | 验证注解的元素值是true |
@DecimalMax(value=n) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值小于等于@ DecimalMax指定的value值 |
@DecimalMin(value=n) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值小于等于@ DecimalMin指定的value值 |
@Digits(integer=整数位数, fraction=小数位数) | BigDecimal,BigInteger,String,byte,short,int,long和原始类型的相应包装。HV额外支持:Number和CharSequence的任何子类型。 | 验证注解的元素值的整数位数和小数位数上限 |
@Future | java.util.Date,java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | 验证注解的元素值(日期类型)比当前时间晚 |
@Past | java.util.Date,java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate; ,则由HV附加支持:ReadablePartial和ReadableInstant的任何实现。 | 验证注解的元素值(日期类型)比当前时间早 |
@Valid | Any non-primitive type(引用类型) | 验证关联的对象,如账户对象里有一个订单对象,指定验证订单对象 |
注:HV ---> Hibernate-Validator
4、分组校验(当我们新增和修改字段时,有可能校验规则是不一样的,那么该如何处理呢?)
(1) 定义分组信息(唯一即可)
package com.ly.valid.group;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 23:10
* @tags 喜欢就去努力的争取
*/
public interface AddGroup {
}
package com.ly.valid.group;
import jakarta.validation.groups.Default;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 23:10
* @tags 喜欢就去努力的争取
*/
public interface UpdateGroup extends Default {
}
(2) 在分组参数后指定它的groups,需要指定在什么情况下需要进行校验,类型是一个接口数组
package com.ly.valid.entity;
import com.ly.valid.constant.RegularConstant;
import com.ly.valid.group.AddGroup;
import com.ly.valid.group.UpdateGroup;
import jakarta.validation.constraints.*;
import jakarta.validation.groups.Default;
import lombok.Data;
import java.time.LocalDate;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:03
* @tags 喜欢就去努力的争取
*/
@Data
public class Person {
@NotBlank(message = "name 姓名不能为空", groups = AddGroup.class)
private String name;
@NotNull(message = "age 年龄不能为空", groups = UpdateGroup.class)
@Min(value = 0, message = "年龄不能小于0")
private Integer age;
@NotNull(message = "gender 性别不能为空", groups = {AddGroup.class, UpdateGroup.class})
private Integer gender;
@NotBlank(message = "email 邮箱不能为空", groups = Default.class)
@Email(regexp = RegularConstant.EMAIL, message = "email 邮箱格式不正确", groups = Default.class)
private String email;
@NotBlank(message = "phone 手机号不能为空", groups = UpdateGroup.class)
@Pattern(regexp = RegularConstant.PHONE, message = "phone 手机号格式不正确")
private String phone;
@Past(message = "birthday 生日日期有误")
private LocalDate birthday;
}
(3) 然后再我们的controller层参数前加上@Validated(value = {UpdateGroup.class})注解,指定它是哪一组
/**
* 测试添加分组
*
* @param person
* @return
*/
@PostMapping("/testAddGroup")
public R testAddGroup(@Validated(AddGroup.class) Person person) {
return R.success(person);
}
/**
* 测试修改分组
*
* @param person
* @return
*/
@PostMapping("/testUpdateGroup")
public R testUpdateGroup(@Validated(UpdateGroup.class) Person person) {
return R.success(person);
}
/**
* 测试添加和修改分组
*
* @param person
* @return
*/
@PostMapping("/testAddAndUpdateGroup")
public R testAddAndUpdateGroup(@Validated({UpdateGroup.class, AddGroup.class}) Person person) {
return R.success(person);
}
/**
* 测试默认分组
*
* @param person
* @return
*/
@PostMapping("/testDefaultGroup")
public R testDefaultGroup(@Validated(Default.class) Person person) {
return R.success(person);
}
(4) 测试AddGroup
(5) 测试UpdateGroup
注意:细心的同学发现了,为什么email邮箱是Default.class,修改的时候也触发了呢?原因如下图:
(6) 测试AddAndUpdateGroup
(7) 测试DefaultGroup
注意:默认没有指定分组的情况下@NotBlank,在分组校验的情况下@Validated(value = {AddGroup.class})不生效
5、自定义校验规则
例如:现在我有一个字段gender
它的取值就三种0:保密 1:男 2:女,像这种有限个数的枚举值我们该如何去限制呢?这就要使用到的自定义校验注解了
(1) 自定义校验注解
package com.ly.valid.anno;
import jakarta.validation.Constraint;
import jakarta.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;
/**
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-12 0:27
* @tags 喜欢就去努力的争取
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
// 默认错误消息
String message() default "{my.enum-value.err.msg}";
// String message() default ENUM_VALUE_MESSAGE;
String[] strValues() default {};
int[] intValues() default {};
// 分组
Class<?>[] groups() default {};
// 负载,可以增加自定义校验逻辑
Class<? extends Payload>[] payload() default {};
// 指定多个时使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
(2) 自定义校验器
package com.ly.valid.anno;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 枚举值校验注解处理类
*
* @author ly (个人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-12 0:34
* @tags 喜欢就去努力的争取
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
/**
* 字符串
*/
private final Set<String> strValueSet = new HashSet<>();
/**
* 数值
*/
private final Set<Integer> intValueSet = new HashSet<>();
@Override
public void initialize(EnumValue constraintAnnotation) {
strValueSet.addAll(Arrays.asList(constraintAnnotation.strValues()));
for (int i : constraintAnnotation.intValues()) {
intValueSet.add(i);
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value instanceof Integer) {
// 整数值类型
return intValueSet.contains(value);
} else if (value instanceof String) {
// 字符串类型
return strValueSet.contains(value);
}
return false;
}
}
(3) 关联自定义的校验器和自定义的校验注解
注意:这里的ValidationMessages.properties是数据校验国际化配置文件;名字必须是这个
my.enum-value.err.msg=性别字段值只能为0、1、2
@EnumValue(intValues = {0, 1, 2}, groups = AddGroup.class)
@NotNull(message = "gender 性别不能为空", groups = {AddGroup.class, UpdateGroup.class})
private Integer gender;
(4) 测试一下
6、paload() 负载的用法
// TODO 未完待续
SpringBoot-Validation优雅实现参数校验的更多相关文章
- SpringBoot Validation优雅的全局参数校验
前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...
- 一起来学SpringBoot(十七)优雅的参数校验
参数校验在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦: 验证代码繁琐,重复劳动方法内代码显得冗长每次要看哪些参数 ...
- 【快学springboot】4.接口参数校验
前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...
- 如何使用java validation api进行参数校验----Hibernate-Validation
在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效. 1. Bean Validation 中内置的 constraint 注解 ...
- 测试开发专题:如何在spring-boot中进行参数校验
上文我们讨论了spring-boot如何去获取前端传递过来的参数,那传递过来总不能直接使用,需要对这些参数进行校验,符合程序的要求才会进行下一步的处理,所以本篇文章我们主要讨论spring-boot中 ...
- SpringBoot Validation参数校验 详解自定义注解规则和分组校验
前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...
- Java web服务端参数校验Javax.validation (springboot)
一.基本使用 Javax.validation是spring集成自带的一个参数校验接口.可通过添加注解来设置校验条件. 下面以springboot项目为例进行说明.创建web项目后,不需要再添加其他的 ...
- 使用Spring Validation优雅地校验参数
写得好的没我写得全,写得全的没我写得好 引言 不知道大家平时的业务开发过程中 controller 层的参数校验都是怎么写的?是否也存在下面这样的直接判断? public String add(Use ...
- SpringBoot - Bean validation 参数校验
目录 前言 常见注解 参数校验的应用 依赖 简单的参数校验示例 级联校验 @Validated 与 @Valid 自定义校验注解 前言 后台开发中对参数的校验是不可缺少的一个环节,为了解决如何优雅的对 ...
- Java Bean Validation(参数校验) 最佳实践
转载来自:http://www.cnblogs.com 参数校验是我们程序开发中必不可少的过程.用户在前端页面上填写表单时,前端js程序会校验参数的合法性,当数据到了后端,为了防止恶意操作,保持程序的 ...
随机推荐
- 从达梦数据库到Oracle数据库的性能测试数据迁移和导入优化
为了在同样的数据基础上对比达梦数据库和Oracle数据库的业务性能,我们需要将达梦数据库的数据导入到Oracle数据库中.本文将提供一种思路来解决导入过程中遇到的问题及存在问题记录. 数据库版本信息 ...
- 《微服务架构设计》——Eventuate Tram框架订阅/消费模式源码解析
Eventuate Tram框架官方文档: https://eventuate.io/docs/manual/eventuate-tram/latest/getting-started-eventua ...
- 10分钟理解契约测试及如何在C#中实现
在软件开发中,确保微服务和API的可靠性和稳定性非常重要. 随着应用程序变得越来越复杂,对强大的测试策略的需求也越来越大,这些策略可以帮助团队在不牺牲敏捷性的情况下交付高质量的代码. 近年来获得广泛关 ...
- Text2Cypher:大语言模型驱动的图查询生成
话接上文<图技术在 LLM 下的应用:知识图谱驱动的大语言模型 Llama Index> 同大家简单介绍过 LLM 和图.知识图谱相关的结合,现在我来和大家分享下最新的成果.毕竟,从 GP ...
- 【Redis】SpringBoot集成Redis事务-亲测
大家好,我是mep.今天一起来探讨一下Redis缓存的问题,SpringBoot如何集成Redis网上文章很多,基本都是介绍如何配置redisTemplate,如何调用,本文就不过多介绍了.这次我们研 ...
- 2023-10-04:用go语言,现有一棵无向、无根的树,树中有 n 个节点,按从 0 到 n - 1 编号 给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges , 其中 edge
2023-10-04:用go语言,现有一棵无向.无根的树,树中有 n 个节点,按从 0 到 n - 1 编号 给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges , 其中 edge ...
- 搞懂闭包JavaScript的GC机制
其实不管什么语言,都有一套垃圾回收机制.为什么要有垃圾回收机制?因为内存,程序运行需要内存,如果没有垃圾回收(循环引用,内存泄漏),那么内存占用就会越来越高,轻点说会影响性能卡顿,严重的直接导致崩溃. ...
- games101-1 光栅化与光线追踪中的空间变换
在学习了一些games101的课程之后,我还是有点困惑,对于计算机图形学的基础知识,总感觉还是缺乏一些更加全面的认识,幸而最*在做games101的第五次作业时,查询资料找到了scratchpixel ...
- ASP.NET Core+Vue3 实现SignalR通讯
从ASP.NET Core 3.0版本开始,SignalR的Hub已经集成到了ASP.NET Core框架中.因此,在更高版本的ASP.NET Core中,不再需要单独引用Microsoft.AspN ...
- Java之引用传递
引用传递分析 类本身就属于引用数据类型,既然是引用数据类型,就会牵扯到内存的引用传递. 引用传递的本质:同一块堆内存空间可以被不同的栈内存所指向,也可以变更指向. 引用传递案例 先看一个应用传递的例子 ...