文章目录
导读
在 JavaEE 项目中, RestFull 层接收参数首先要对一些字段的格式进行校验,以防止所有查询都落到数据库,这也是一种合理的限流手段。以前基本上都是用 if…else…,这样的代码太啰嗦,除了使用策略模式进行优化,今天介绍一下校验注解@Valid,@Validated和@PathVariable,不仅可以减轻代码量,还加强了代码的易读性。
1. @Valid 和 @Validated 区别与联系
Hibernate Validator 是 Bean Validation 的参考实现 。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
在SpringBoot中可以使用@Validated,注解Hibernate Validator加强版,也可以使用@Valid原来Bean Validation java版本
先讲一下这两个注解:@Valid与@Validated都是用来校验接收参数的,如果不使用注解校验参数,那么就需要在业务代码中逐一校验,这样会增加很多的工作量,并且代码不优美。
刚开始接触的时候多半会被弄混,实际上二者差距还是挺大的。根据自己的项目经验,@Validated和@Valid各有特点,可以联合使用。
校验流程解析
使用 Validation API 进行参数效验步骤整个过程如下图所示,用户访问接口,然后进行参数效验 ,如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理
(1)提供者
Validated
即org.springframework.validation.annotation.Validated
只用 Spring Validator 校验机制使用,是Spring 做得一个自定义注解
,增强了分组功能;
Valid
即javax.validation.Valid。 使用 Hibernate validation 的时候使用,是
JSR-303规范标准注解支持
。如果你是 springboot 项目,那么可以不用单独引入依赖了,因为它就存在于最核心的 web开发包(spring-boot-starter-web)里面;
(2)标注位置
@Validated:可以用在类型、方法和方法参数上,不能用于成员属性(field)上。如果注解在成员属性上,则会报不适用于field的错误;
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)、YPE_US上;
注:TYPE_USE:在 Java 8 之前的版本中,只能允许在声明式前使用 Annotation。而在 Java 8 版本中,Annotation 可以被用在任何使用 Type 的地方,例如:初始化对象时 (new),对象类型转化时,使用 implements 表达式时,或者使用 throws 表达式时。
//初始化对象时
String myString = new @Valid String();
//对象类型转化时
myString = (@Valid String) str;
//使用 implements 表达式时
class MyList<T> implements List<@Valid T> {...}
//使用 throws 表达式时
public void validateValues() throws @Valid ValidationFailedException{...}
(3)分组支持
@Validated:
提供分组功能
,可以在参数验证时,根据不同的分组采用不同的验证机制;
@Valid:没有分组的功能
,不能进行分组校验;
(4)嵌套支持
@Validated:不能进行嵌套对象校验;
@Valid:可以进行嵌套校验,但是需要在嵌套的字段上面加上注解;
2. 常用的校验方法
Maven依赖的引入:
springboot 2.3.0 以后不会自动引入jar包,所以要添加以下maven,springboot2.3以前则不需要引入maven包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
搭配@Valid或者@Validated使用
@Valid、@Validated 注解可以实现数据的验证,你可以定义实体,在实体的属性上添加校验规则
@Valid 包位置
import javax.validation.Valid;
@Validated 包位置
import org.springframework.validation.annotation.Validated;
Debug进入jar包,可以看到全量的相关注解:
简述一些常用注解:
空检查
@Null: 所注解的元素必须为 null,接受任何类型。
@NotNull:
(1)限制必须不能为null,但可以为 empty, 可以接受任何类型。
(2)empty:空集合、空数组、空字符等等
(2)一般用于判空 Integer 类型等基本数据类型,而且被其标注的字段可以使用@size、@Max、@Min 对数值进行大小的控制
@NotEmpty:
(1)验证注解的元素值不为null且不为空
(2)不为空:即长度必须大于0,字符串长度不为0、集合大小不为0
(3)一般用于集合类或者数组上,也有人用于String(不推荐)。比如校验 CharSequence、数组、Collection 和 Map。
@NotBlank:
(1)验证注解的元素值不为空且不能为空字符。即:必须有实际字符
(2)不能为空字符:调用 trim() 方法后,长度必须大于0。
(3)不同于@NotEmpty,@NotBlank注解只应用于String 类型上,且在比较时会去除字符串的空格
(4)用于校验CharSequence(含String、StringBuilder和StringBuffer)。只支持字符类型。
Boolean检查
@AssertTrue:被注释的元素必须为true
@AssertFalse:被注释的元素必须为false
长度检查
@Size(max, min): 被注释的元素的大小必须在指定的范围内。一般用于限制数组、集合长度范围必须在min到max之间。也可以用于限制字符串在指定的范围内(不推荐)
@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内,即验证注解的元素值长度在min和max区间内,只能用于字符串
日期检查:Date/Calendar
@Past:被注释的元素必须是一个过去的日期
@PastOrPresent: 验证元素值必须是当前时间或一个过去的时间,认为 null 是有效的
@Future:被注释的元素必须是一个将来的日期
@FutureOrPresent: 验证元素值必须是当前时间或一个将来的时间,认为 null 是有效的
数值检查:建议使用在Stirng,Integer类型
@Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits:验证是否为数字
@Digits(integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Positive:验证元素必须为正数,认为 null 是有效的。BigDecimal、BigInteger,byte、short、int、long、float、double 及其包装类型
@PositiveOrZero: 同Positive,验证元素必须为正数或 0,认为 null 是有效的
@Negative: 验证元素必须为负数,认为 null 是有效的
@NegativeOrZero: 验证元素必须为负数或 0,认为 null 是有效的
@Range(min=long1,max=long2):被注释的元素必须在合适的范围内,即检查数字是否在范围min到max之间
该注解可以用来验证数字与字符串类型,数值或者字符串的值必须在 min 和 max 指定的范围内
字符验证
@URL(protocol=协议,host=主机,port=端口号,regexp=正则表达式,flags=标识):用于校验一个字符串是否是合法URL
@Pattern(regexp=“reg”):被注释的元素必须符合指定的正则表达式。
注意:如果 @Pattern 所注解的元素是null,则@Pattern 注解会返回 true,即也会通过校验,所以应该把 @Pattern 注解和 @NotNull 注解结合使用。
其他验证
@Vaild:递归验证,用于任何非原子类型,比如用于对象、数组和集合,会对对象的元素、数组的元素进行一一校验
@Email:用于验证一个字符串是否是一个合法的右键地址,空字符串或null算验证通过
message属性
(1)message支持表达式和EL表达式 ,比如message = “姓名长度限制为{min}到{max} ${1+2}”)
(2)几乎所有的校验注解都有该注解
(3)如果想把错误描述统一写到properties的话,在classpath下面新建
ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符
3. @Validated分组校验
场景:多个 Restfull 接口共用一个标准 Bean,每个接口的参数相同,但是需要校验的参数(必输项)却不完全相同,这样的场景可以使用 @Validated,因为它提供了分组校验的功能。
注意:
(1)没有指定显示分组的被校验字段和校验注解,默认都是 Default 组(即 Default.class)
(2)若自定义的分组接口未继承 Default 分组,且 @Validated(或 @Valid)注解未传参 Default.class,则只会校验请求对象中进行了显示分组的字段,不会校验默认分组(没有进行显示分组)的字段
(3)自定义的分组接口不继承 Default 分组 + @Validated(或 @Valid)注解传参 {自定义分组接口.class, Default.class}
(4)= 自定义的分组接口继承 Default 分组 + @Validated(或 @Valid)注解只传参自定义分组接口
案例1
如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name字段为空,但是在更新时候不允许将名字更新为空字符。
分组校验有三个步骤:
第一步:定义一个分组类(或接口)
public interface Update extends Default{
}
第二步:在校验注解上添加groups属性指定分组
public class UserVO {
@NotBlank(message = "name 不能为空",groups = Update.class)
private String name;
// 省略其他代码...
}
第三步:Controller方法的@Validated注解添加分组类
@PostMapping("update")
public ResultInfo update(@Validated({Update.class}) UserVO userVO) {
return new ResultInfo().success(userVO);
}
自定义的Update分组接口继承了Default接口。校验注解(如: @NotBlank)和@validated默认其他注解都属于Default.class分组,这一点在javax.validation.groups.Default注释中有说明
/**
* Default Jakarta Bean Validation group.
* <p>
* Unless a list of groups is explicitly defined:
* <ul>
* <li>constraints belong to the {@code Default} group</li>
* <li>validation applies to the {@code Default} group</li>
* </ul>
* Most structural constraints should belong to the default group.
*
* @author Emmanuel Bernard
*/
public interface Default {
}
在编写Update分组接口时,如果继承了Default,下面两个写法就是等效的:
@Validated({Update.class}),@Validated({Update.class,Default.class})
如果Update不继承Default,@Validated({Update.class})就只会校验属于Update.class分组的参数字段
案例2
第一步:新建请求对象
@Data
public class TeacherDTO {
@NotBlank(message = "id必传")
private String id;
@NotBlank(message = "不能没有名称")
private String name;
@NotNull(message = "age必传")
private Integer age;
@NotBlank(message = "不能没有idCard")
private String idCard;
@NotBlank(message = "老师不能没有手机号", groups = OnlyTeacher.class)
private String phone;
@NotEmpty(message = "学生不能没有书")
@Size(min = 2, message = "学生必须有两本书", groups = OnlyStudent.class)
private List<String> bookNames;
@NotEmpty
@Size(min = 1, message = "老师不能没有学生", groups = TeacherWithDefault.class)
private List<String> studentList;
}
第二步:新建分组
// Teacher分组
public interface TeacherValid { }
// Student分组
public interface StudentValid { }
// 继承Default的分组
public interface OtherValid extends Default{ }
第三步:接口测试
/**
* Created by tjm on 2022/11/11.
*/
@RestController
@RequestMapping("/test")
public class TestValidController {
private static final Logger LOGGER = LoggerFactory.getLogger(TestValidController.class);
/**
* 测试 - 分组校验 - 默认default
*/
@PostMapping("/only/default")
public Object testDefaultValid(@Validated TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 只有teacher
*/
@PostMapping("/only/teacher")
public Object testOnlyTeacherValid(@Validated(OnlyTeacher.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 只有student
*/
@PostMapping("/only/student")
public Object testOnlyStudentValid(@Validated(OnlyStudent.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - teacher + default
*/
@PostMapping("/with/teacher")
public Object testWithTeacherValid(@Validated({OnlyTeacher.class, Default.class}) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
/**
* 测试 - 分组校验 - 继承default
*/
@PostMapping("/with/default")
public Object testWithDefaultValid(@Validated(TeacherWithDefault.class) TeacherDTO param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
}
结果
案例3
分组使用
有时在同一个 PO 类上,同一个属性在不同的请求中验证规则不一样,比如 ID, 在新增时可以为空,但是在更新是不能为空。这种校验方式可以使用 @Validated 根据分组指定校验规则。
1、定义分组
// 分组必须为接口,它只是一个标记类,不需要有任何方法
// 更新分组,
public interface Update {
}
// 新增分组
public interface Add {
}
2、PO指定分组
@Data
public class BannersEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 编号
* 指定校验分组为 Update
*/
@TableId
@NotNull(message = "id不能为空", groups = {Update.class}) //
private Long id;
/**
* 图片名称
* 指定校验分组为 Update 和 add
*/
@NotBlank(message = "图片名称不能为空", groups = {Add.class, Update.class})
private String name;
}
3、请求指定分组
/**
* 保存
* 校验器 @Validated 指定 Add 分组
*/
@RequestMapping("/save")
public R save(@RequestBody
@Validated(value = {Add.class}) BannersEntity banners) {
bannersService.save(banners);
return R.ok();
}
/**
* 修改
* 校验器 @Validated 指定 Update 分组
*/
@RequestMapping("/update")
public R update(@RequestBody @Validated(value = {Update.class}) BannersEntity banners) {
bannersService.updateById(banners);
return R.ok();
}
4. @Valid嵌套校验
@Valid: 嵌套校验 = 入参@Valid + 属性上@Valid
用在方法入参上无法单独提供嵌套验证功能。
能配合嵌套验证注解@Valid进行嵌套验证。
能够用在成员属性(字段)上,提示验证框架进行嵌套验证。
@Validated: 嵌套校验 = 入参@Validated + 属性上@Valid
用在方法入参上无法单独提供嵌套验证功能。
能配合嵌套验证注解@Valid进行嵌套验证。
不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。
注意:
(1)支持外部类嵌套校验 (2)同时也支持内部类嵌套校验
新建请求对象
public class Item {
@NotNull(message = "id不能为空")
@Min(value = 1, message = "id必须为正整数")
private Long id;
// 嵌套验证必须用 @Valid
@Valid
@NotNull(message = "props不能为空")
@Size(min = 1, message = "props至少要有一个自定义属性")
private List<Prop> props;
}
public class Prop {
@NotNull(message = "pid不能为空")
@Min(value = 1, message = "pid必须为正整数")
private Long pid;
@NotNull(message = "vid不能为空")
@Min(value = 1, message = "vid必须为正整数")
private Long vid;
@NotBlank(message = "pidName不能为空")
private String pidName;
@NotBlank(message = "vidName不能为空")
private String vidName;
}
接口测试
/**
* 测试 - 分组校验 - 继承default
*/
@PostMapping("/item")
public Object testItemValid(@Validated Item param, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResultGenerator.genFailResult(bindingResult.getFieldError().getDefaultMessage());
}
return ResultGenerator.genSuccessResult();
}
测试结果
1. 不仅校验 Item 参数,还会校验子类 Prop 参数;
2. 注意:嵌套验证必须在子参数上用 @Valid。
只是在方法参数前面添加 @Valid和 @Validated注解,不会对嵌套的实体类进行校验.要想实现对嵌套的实体类进行校验,需要在嵌套的实体类属性上添加 @Valid注解
5. 自定义校验
校验器的格式
public class XXXValidator implements ConstraintValidator<CheckXXX, Object> {
private String name;
private int age;
@Override
public void initialize(CheckTimeInterval constraintAnnotation) {
this.name = constraintAnnotation.name();
this.age = constraintAnnotation.age();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
BeanWrapper beanWrapper = new BeanWrapperImpl(o);
String name = (String)beanWrapper.getPropertyValue(name);
int age = (Integer)beanWrapper.getPropertyValue(age);
return age > 10 && name.equals("abc");
}
}
- ConstraintValidator<>:第二个参数表示注解修饰的对象类型。(类注解就是Object)
- initialize():类初始化方法。
- isValid():返回true表示校验通过,返回false表示校验失败,返回message的错误提示信息。
当组件提供的校验器, 不满足需求时,可以自定义校验器,步骤如下:
1、定义校验注解
// 定义 MyValid 校验主机
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) // 指定可以使用的目标
@Retention(RetentionPolicy.RUNTIME) //
@Constraint(validatedBy = {MyValidConstraint.class}) //指定校验器
public @interface MyValid {
// 其中
String message() default "校验失败"; // 校验不通过时的提示信息
Class<?>[] groups() default {}; // 分组
Class<? extends Payload>[] payload() default {}; // 载体
}
说明:自定义校验器必须要有 message,groups,payload 这三项,此外可以进行扩展
2、定义校验器
// 实现接口 ConstraintValidator
public class MyValidConstraint implements ConstraintValidator<MyValid, Object> {
private MyValid myValid;
@Override
public void initialize(MyValid myValid) {
this.myValid = myValid;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// TODO 此处些具体校验逻辑
return true; // 返回 true 表示验证通过,false 表示验证失败
}
}
案例1
第一步:自定义注解
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE,
ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckTimeIntervalValidation.class)
@Documented
@Repeatable(CheckTimeInterval.List.class)
public @interface CheckTimeInterval {
/**
* 开始日期
* @return field
*/
String beginTime() default "startTime";
/**
* 查询参数时间类型
* @return field
*/
String timeType() default "LocalDateTime";
/**
* 结束日期
* @return field
*/
String endTime() default "endTime";
/**
* 日期间隔
* @return field
*/
int dayRange() default 30;
String message() default "{org.hibernate.validator.referneceguide.chapter06.CheckCase.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
CheckTimeInterval[] value();
}
}
第二步:自定义解析注解工具类
public class CheckTimeIntervalValidation implements ConstraintValidator<CheckTimeInterval, Object> {
private String beginTime;
private String endTime;
private int dayRange;
private String timeType;
@Override
public void initialize(CheckTimeInterval constraintAnnotation) {
this.beginTime = constraintAnnotation.beginTime();
this.endTime = constraintAnnotation.endTime();
this.dayRange = constraintAnnotation.dayRange();
this.timeType=constraintAnnotation.timeType();
}
@Override
public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
BeanWrapper beanWrapper = new BeanWrapperImpl(o);
long result=0;
if ("LocalDateTime".equals(timeType)){
LocalDateTime begin = (LocalDateTime) (beanWrapper.getPropertyValue(beginTime));
LocalDateTime end = (LocalDateTime) (beanWrapper.getPropertyValue(endTime));
if (null == begin || null == end) {
return false;
}
// result = end.compareTo(begin);
result=begin.until(end, ChronoUnit.DAYS);
}else if ("LocalDate".equals(timeType)){
LocalDate begin = (LocalDate) (beanWrapper.getPropertyValue(beginTime));
LocalDate end = (LocalDate) (beanWrapper.getPropertyValue(endTime));
if (null == begin || null == end) {
return false;
}
result=begin.until(end, ChronoUnit.DAYS);
}
return result>=0&&result <= dayRange;
}
}
第三步:测试
@CheckTimeInterval(beginTime = "start",endTime = "end",timeType="LocalDateTime"
,dayRange = 14,message = "开始时间必须小于结束时间,只能查询14天的日期范围数据")
@CheckTimeInterval(beginTime = "start",endTime = "end",timeType="LocalDateTime"
,dayRange = 28,message = "开始时间必须小于结束时间,只能查询28天的日期范围数据")
public class RunningRecordQuery {
...
}
参考:https://blog.csdn.net/Lemon_MY/article/details/128645810
案例2
validation 为我们提供了这么多特性,几乎可以满足日常开发中绝大多数参数校验场景了。但是一个好的框架一定是方便扩展的。有了扩展能力,就能应对更多复杂的业务场景,毕竟在开发过程中,唯一不变的就是变化本身。 Validation允许用户自定义校验
实现步骤很简单分两步:
第一步:创建一个自定义校验注解
package cn.soboys.core.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* @author kenx
* @version 1.0
* @date 2021/1/21 20:49
* 日期验证 约束注解类
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsDateTimeValidator.class}) // 标明由哪个类执行校验逻辑
public @interface IsDateTime {
// 校验出错时默认返回的消息
String message() default "日期格式错误";
//分组校验
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
//下面是我自己定义属性
boolean required() default true;
String dateFormat() default "yyyy-MM-dd";
}
注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = {
HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类
第二步:编写校验者类
package cn.soboys.core.validator;
import cn.hutool.core.util.StrUtil;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* @author kenx
* @version 1.0
* @date 2021/1/21 20:51
* 日期验证器
*/
public class IsDateTimeValidator implements ConstraintValidator<IsDateTime, String> {
private boolean required = false;
private String dateFormat = "yyyy-MM-dd";
/**
* 用于初始化注解上的值到这个validator
* @param constraintAnnotation
*/
@Override
public void initialize(IsDateTime constraintAnnotation) {
required = constraintAnnotation.required();
dateFormat = constraintAnnotation.dateFormat();
}
/**
* 具体的校验逻辑
* @param value
* @param context
* @return
*/
public boolean isValid(String value, ConstraintValidatorContext context) {
if (required) {
return ValidatorUtil.isDateTime(value, dateFormat);
} else {
if (StrUtil.isBlank(value)) {
return true;
} else {
return ValidatorUtil.isDateTime(value, dateFormat);
}
}
}
}
注意这里验证逻辑我抽出来单独写了一个工具类,ValidatorUtil
package cn.soboys.core.validator;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author kenx
* @version 1.0
* @date 2021/1/21 20:51
* 验证表达式
*/
public class ValidatorUtil {
private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");
private static final Pattern money_pattern = Pattern.compile("^[0-9]+\\.?[0-9]{0,2}$");
/**
* 验证手机号
*
* @param src
* @return
*/
public static boolean isMobile(String src) {
if (StrUtil.isBlank(src)) {
return false;
}
Matcher m = mobile_pattern.matcher(src);
return m.matches();
}
/**
* 验证枚举值是否合法 ,所有枚举需要继承此方法重写
*
* @param beanClass 枚举类
* @param status 对应code
* @return
* @throws Exception
*/
public static boolean isEnum(Class<?> beanClass, String status) throws Exception {
if (StrUtil.isBlank(status)) {
return false;
}
//转换枚举类
Class<Enum> clazz = (Class<Enum>) beanClass;
/**
* 其实枚举是语法糖
* 是封装好的多个Enum类的实列
* 获取所有枚举实例
*/
Enum[] enumConstants = clazz.getEnumConstants();
//根据方法名获取方法
Method getCode = clazz.getMethod("getCode");
Method getDesc = clazz.getMethod("getDesc");
for (Enum enums : enumConstants) {
//得到枚举实例名
String instance = enums.name();
//执行枚举方法获得枚举实例对应的值
String code = getCode.invoke(enums).toString();
if (code.equals(status)) {
return true;
}
String desc = getDesc.invoke(enums).toString();
System.out.println(StrFormatter.format("实列{}---code:{}desc{}", instance, code, desc));
}
return false;
}
/**
* 验证金额0.00
*
* @param money
* @return
*/
public static boolean isMoney(BigDecimal money) {
if (StrUtil.isEmptyIfStr(money)) {
return false;
}
if (!NumberUtil.isNumber(String.valueOf(money.doubleValue()))) {
return false;
}
if (money.doubleValue() == 0) {
return false;
}
Matcher m = money_pattern.matcher(String.valueOf(money.doubleValue()));
return m.matches();
}
/**
* 验证 日期
*
* @param date
* @param dateFormat
* @return
*/
public static boolean isDateTime(String date, String dateFormat) {
if (StrUtil.isBlank(date)) {
return false;
}
try {
DateUtil.parse(date, dateFormat);
return true;
} catch (Exception e) {
return false;
}
}
}
我自定义了补充了很多验证器,包括日期验证,枚举验证,手机号验证,金额验证
自定义校验注解使用起来和内置注解无异,在需要的字段上添加相应注解即可
6. Restfull层@Validated的使用
校验参数的时候,如何判断并返回失败的结果?一般有两种方式:
1. 全局异常捕获
@ControllerAdvice
@RestController
@Slf4j
public class GlobalExceptionHandler {
/**
* 非法参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(value = HttpStatus.OK)
public ApiResult handleMethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
List<String> list = new ArrayList<>();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors) {
list.add(fieldError.getDefaultMessage());
}
Collections.sort(list);
log.error("fieldErrors" + JSON.toJSONString(list));
return ApiResult.fail(ApiCode.PARAMETER_EXCEPTION, list);
}
}
2. 用 BindingResult 在实体类校验信息返回结果绑定
即使是全局异常捕获的方式,也能看到:校验信息是被封装在 BindingResult 对象里的,所以,我们也可以在 RestFull 层直接取。
(1) BindingResult用在实体类校验信息返回结果绑定;
(2) BindingResult.hasErrors()判断是否校验通过,bindingResult.getFieldError().getDefaultMessage()
获取在 TestEntity 的属性设置的自定义message,如果没有设置,则返回默认值 “javax.validation.constraints.XXX.message”。
可以看到,我上面的例子用的都是这种方法,我觉得这样更方便、直观,维护性更好。
- restful风格用法
在多个参数校验,或者@RequestParam 形式时候,需要在controller上加注@Validated
@GetMapping("/get")
public RspDTO getUser(@RequestParam("userId") @NotNull(message = "用户id不能为空") Long userId) {
User user = userService.selectById(userId);
if (user == null) {
return new RspDTO<User>().nonAbsent("用户不存在");
}
return new RspDTO<User>().success(user);
}
@RestController
@RequestMapping("user/")
@Validated
public class UserController extends AbstractController {
}
7、案例
7.1 初级约束注解:
- @NotNull(message = “用户id不能为空”)
(1) 没有设置值时会返回
(2)设置为userInfo.setUserId("");时不会返回
(3)userInfo.setUserId(" ");时不会返回
2.@NotEmpty(message = “用户名不能为空”)
(1)不会自动去掉字符串前后的空格再判断是否为空
(2)没有设置值时会返回
(3) userInfo.setUserName("");时返回
(4)userInfo.setUserName(" ");时不返回
3.@NotBlank(message = “密码不能为空”)
(1)自动去掉字符串前后的空格再判断是否为空
(2)没有设置值时会返回
(3)userInfo.setPassWord("");时返回
(4)userInfo.setPassWord(" ");时返回
4.@Length(min = 6,max = 20,message = “密码不能少于6位,也不能多于20位”)
(1)可以直接不设置值
(2)即数量必须在6到20之间(包含6和20)
5.@Email(message = “邮箱不正确”)
(1)可以直接不设置值
(2)必须是正确的邮箱格式
- @Min(value = 18, message = “年龄不能小于18岁”)
(1)可以直接不设置值
(2)即年龄必须大于或者等于18岁
- @Max(value = 60, message = “年龄不能大于60岁”)
(1)可以直接不设置值
(2)即年龄必须等于小于60岁
8.@Past(message = “生日只能是过去的时间或者现在的时间”)
(1)可以直接不设置值
(2)传来的时间只能是过去的时间或者现在的时间,不能是未来时间
9.@Size(min = 1,message = “不能少于一个好友”)
(1)可以直接不设置值
(2)集合里面的内容不能少于1
7.2. 中级约束注解:
1.@Valid
private List<@Valid UserInfo> friends;表示对UserInfo类中里面的每个属性进行验证
2.@NotNull(message = “注册时邮箱不能为空”,groups = RegisterGroup.class)
注册场景 public interface RegisterGroup{}
登录场景 public interface LoginGroup{}
注册时邮箱不能为空,登录时可以为空
set = validator.validate(userInfo,UserInfo.RegisterGroup.class);
3.组排序场景
@GroupSequence({
LoginGroup.class,
RegisterGroup.class,
Default.class
})
public interface Group{}
(1)set = validator.validate(userInfo,UserInfo.Group.class);
(2)先验证 LoginGroup组的,都通过才验证下面的,不通过直接返回验证错误信息,验证顺序按照上面的顺序进行验证
7.3. 高级约束注解:
@Valid对方法输入参数进行约束注解校验
public void setUserInfo(@Valid UserInfo userInfo){ }
set1 = executableValidator.validateParameters(service,method1,paramObject);
@Valid对方法返回值进行约束校验
public @Valid UserInfo getUserInfo(){return new UserInfo();}
set1 = executableValidator.validateReturnValue(userInfoService,method,returnValue);
@Valid对构造函数输入参数进行校验
public UserInfoService(@Valid UserInfo userInfo){}
set1 = executableValidator.validateConstructorParameters(constructor,paramObject);
7.4. 上面初,中,高级注解示例
pom.xml
<!-- Validation 相关依赖 -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.16.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
待验证对象实体类UserInfo.java:
package com.mystudy.elastic.job.springboot.validation;
import org.hibernate.validator.constraints.Length;
import javax.validation.GroupSequence;
import javax.validation.Valid;
import javax.validation.constraints.*;
import javax.validation.groups.Default;
import java.util.Date;
import java.util.List;
/**
* 待验证对象实体类
* 用户信息类
*/
public class UserInfo {
//登录场景
public interface LoginGroup{}
//注册场景
public interface RegisterGroup{}
//组排序场景
@GroupSequence({
LoginGroup.class,
RegisterGroup.class,
Default.class
})
public interface Group{}
/**
* 用户id
*/
@NotNull(message = "用户id不能为空")
private String userId;
/**
* 用户名
*/
@NotEmpty(message = "用户名不能为空")
private String userName;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Length(min = 6, max = 20, message = "密码不能少于6位,也不能多于20位")
private String passWord;
/**
* 邮箱
*/
// @NotNull(message = "注册时邮箱不能为空",groups = RegisterGroup.class)
@Email(message = "邮箱不正确")
private String email;
/**
* 年龄
*/
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 60, message = "年龄不能大于60岁")
private Integer age;
/**
* 手机号
*/
@Phone(message = "手机号不是158后面随便的手机号")
private String phone;
/**
* 生日
*/
@Past(message = "生日只能是过去的时间或者现在的时间")
private Date birthday;
/**
* 好友列表
*/
@Size(min = 1,message = "不能少于一个好友")
private List<@Valid UserInfo> friends;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassWord() {
return passWord;
}
public void setPassWord(String passWord) {
this.passWord = passWord;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public List<UserInfo> getFriends() {
return friends;
}
public void setFriends(List<UserInfo> friends) {
this.friends = friends;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
用户信息服务类UserInfoService.java
package com.mystudy.elastic.job.springboot.validation;
import javax.validation.Valid;
/**
* 用户信息服务类
*/
public class UserInfoService {
/**
* userInfo作为输入参数
* @param userInfo
*/
public void setUserInfo(@Valid UserInfo userInfo){
}
/**
* userInfo作为输出参数
* @return
*/
public @Valid UserInfo getUserInfo(){
return new UserInfo();
}
/**
* 默认构造函数
*/
public UserInfoService(){
}
/**
* 接收userInfo作为参数的构造函数
* @param userInfo
*/
public UserInfoService(@Valid UserInfo userInfo){
}
}
验证测试类ValidationTest.java:
package com.mystudy.elastic.job.springboot.validation;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.executable.ExecutableValidator;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.Set;
/**
* 验证测试类
*/
public class ValidationTest {
//验证器对象
private Validator validator;
//待验证对象
private UserInfo userInfo;
//验证结果集合
private Set<ConstraintViolation<UserInfo>> set;
//验证结果集合
private Set<ConstraintViolation<UserInfoService>> set1;
/**
* 初始化操作
*/
@Before
public void init() {
//初始化验证器
validator = Validation.buildDefaultValidatorFactory().getValidator();
//初始化待验证对象 用户信息
userInfo = new UserInfo();
userInfo.setUserId(" ");
userInfo.setUserName(" ");
userInfo.setPassWord(" ");
userInfo.setPassWord("333333");
userInfo.setAge(18);
userInfo.setBirthday(new Date());
userInfo.setPhone("15987377373");
UserInfo friend = new UserInfo();
friend.setUserId("wangxiaoxi");
friend.setUserName("王小喜");
friend.setPassWord("wangxiaoxi");
userInfo.setFriends(new ArrayList() {{
add(friend);
}});
}
/**
* 结果打印
*/
@After
public void print() {
set.forEach(item -> {
//输出验证错误信息
System.out.println(item.getMessage());
});
// set1.forEach(item -> {
// //输出验证错误信息
// System.out.println(item.getMessage());
// });
}
@Test
public void nullValidation() {
//使用验证器对对象进行验证
set = validator.validate(userInfo);
}
/**
* 级联验证测试方法
*/
@Test
public void graphValidation() {
set = validator.validate(userInfo);
}
/**
* 验证注册时,邮箱是否为空
*/
@Test
public void groupValidation() {
set = validator.validate(userInfo, UserInfo.RegisterGroup.class);
}
/**
* 组排序
*/
@Test
public void groupSequenceValidate() {
set = validator.validate(userInfo, UserInfo.Group.class);
}
/**
* 对方法输入参数进行约束注解校验
*/
@Test
public void paramValidation() throws NoSuchMethodException {
//获取校验执行器
ExecutableValidator executableValidator = validator.forExecutables();
//待验证对象
UserInfoService service = new UserInfoService();
//待验证方法
Method method1 = service.getClass().getMethod("setUserInfo", UserInfo.class);
//方法的输入参数
Object[] paramObject = new Object[]{new UserInfo()};
//对方法的输入参数进行校验
set1 = executableValidator.validateParameters(service, method1, paramObject);
}
/**
* 对方法返回值进行约束校验
*/
@Test
public void returnValueValidation() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//获取校验执行器
ExecutableValidator executableValidator = validator.forExecutables();
//构造要验证的方法对象
UserInfoService userInfoService = new UserInfoService();
Method method = userInfoService.getClass().getMethod("getUserInfo");
//调用方法得到返回值
Object returnValue = method.invoke(userInfoService);
//校验方法返回值是否符合约束
set1 = executableValidator.validateReturnValue(userInfoService, method, returnValue);
}
/**
* 对构造函数输入参数进行校验
*/
@Test
public void constructorValidation() throws NoSuchMethodException {
//获取验证执行器
ExecutableValidator executableValidator = validator.forExecutables();
//获取构造函数
Constructor<UserInfoService> constructor = UserInfoService.class.getConstructor(UserInfo.class);
Object[] paramObject = new Object[]{new UserInfo()};
//校验构造函数
set1 = executableValidator.validateConstructorParameters(constructor, paramObject);
}
}
鸣谢: https://blog.csdn.net/weixin_44259720/article/details/127965610
https://zhuanlan.zhihu.com/p/387776766
原文链接:https://blog.csdn.net/weixin_45703155/article/details/130001434