前言:
校验参数在以前基本都是使用大量的if/else
,稍微方便一点的可以使用反射+自定义注解
的形式,但是复用性不是很好,并且每个人对于的自定义注解有着自己的使用习惯,不过好在spring开发了validated框架用于注解校验,可以节省很多的校验ifelse
代码,这篇文章通篇介绍了如何使用spring validated。
文章目的:
- 了解 validate 校验,快速的集成和使用 spring validate,以及使用注解对于参数进行快速校验
- 关于统一全局异常处理,以及一些踩坑问题。
- 讨论
list<Object>
校验的问题,分析如何使用list对象内容校验
简单介绍
spring Validation
是一种参数检验工具,集成在spring-context包中, 常用于spring mvc
中Controller
的参数处理,主要针对整个实体类的多个可选域进行判定,对于不合格的数据信息springMVC会把它保存在错误对象中,这些错误信息我们也可以通过SpringMVC
提供的标签或者前端的脚本等在前端页面上进行展示。
实现方式和使用方式:一般使用较多的是两个注解:@Validated
、@Valid
- 第一种使用方式:使用
Validator
,利用BindingResult
获取Errors信息 - 第二种使用方式:采用
@Valid
以及JSR-303(@validated)
中的参数判定注解
@Valid
和@Validated
区别
区别 | @Valid | @Validated |
提供者 | JSR-303规范 | Spring |
是否支持分组 | 不支持 | 支持 |
标注位置 | METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE | TYPE, METHOD, PARAMETER |
嵌套校验 | 支持 | 不支持 |
常用注解
网上有很多类似表格,这里直接COPY
了一份,对于不同的api版本可能出现部分注解过时等情况,注意!
meta-data | comment | version |
@Null | 对象,为空 | Bean Validation 1.0 |
@NotNull | 对象,不为空 | Bean Validation 1.0 |
@AssertTrue | 布尔,为True | Bean Validation 1.0 |
@AssertFalse | 布尔,为False | Bean Validation 1.0 |
@Min(value) | 数字,最小为value | Bean Validation 1.0 |
@Max(value) | 数字,最大为value | Bean Validation 1.0 |
@DecimalMin(value) | 数字,最小为value | Bean Validation 1.0 |
@DecimalMax(value) | 数字,最大为value | Bean Validation 1.0 |
@Size(max, min) | min<=value<=max | Bean Validation 1.0 |
@Digits (integer, fraction) | 数字,某个范围内 | Bean Validation 1.0 |
@Past | 日期,过去的日期 | Bean Validation 1.0 |
@Future | 日期,将来的日期 | Bean Validation 1.0 |
@Pattern(value) | 字符串,正则校验 | Bean Validation 1.0 |
字符串,邮箱类型 | Bean Validation 2.0 | |
@NotEmpty | 集合,不为空 | Bean Validation 2.0 |
@NotBlank | 字符串,不为空字符串 | Bean Validation 2.0 |
@Positive | 数字,正数 | Bean Validation 2.0 |
@PositiveOrZero | 数字,正数或0 | Bean Validation 2.0 |
@Negative | 数字,负数 | Bean Validation 2.0 |
@NegativeOrZero | 数字,负数或0 | Bean Validation 2.0 |
@PastOrPresent(时间) | 过去或者现在 | Bean Validation 2.0 |
@FutureOrPresent(时间) | 将来或者现在 | Bean Validation 2.0 |
快速入门
spring validate
入门使用都十分的简单,基本十分钟不到就能快速的集成,目前springboot的项目已经越来越多,所以本文基本都是基于springboot构建的,spring mvc
集成和本文类似。
第一步:pom.xml 加入注解
这里为了方便版本控制增加了版本控制配置:
注意:hibernate-validate 的版本到本文为止已经出现了7.0.0,这个版本的校验做了不少的改动。
<properties> <hibernate-validate.version>5.2.0.Final</hibernate-validate.version> </properties> 复制代码
增加完配置之后,增加对应的maven依赖,需要引入如下两个依赖配置
<!--jsr 303--> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <!-- hibernate validator--> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <!-- 注意使用了版本控制 --> <version>${hibernate-validate.version}</version> </dependency> 复制代码
加完这两个注解之后,不要急着先进行其他的编写,可以现在任意的class类里面敲击上面提到的注解,看下是不是有对应的注解了,在做下一步。
第二步:增加注解
这里给出一个案例进行参考,验证对象增加注解:
@NotBlank
:字符串不能为Null以及不能为空字符串,建议String
都使用这种类型
@Length(max = 7)
:max
和min
表示字符的最大长度,这里多提一嘴的是接口一定要加上这种注解,注意是一定,因为接口给参数不加以长度限制就等着数据库截断二进制
吧。
@Pattern
:根据案例就可以看到,就是JAVA对应的Pattern
,可以编写正则进行校验,很棒,因为Pattern
对象比较占用空间。目前个人没有验证过这个注解的校验性能问题,有想法可以自己测试一下。
public class TestProduct { @NotBlank @Length(max = 7) @Pattern(regexp = "^(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){0,2})?$") private String tranAmount; @NotBlank @Length(max = 3,min = 3) @Pattern(regexp = "^[A-Z]{3}$") private String currency; @NotBlank @Length(max = 100) private String tranReason; @NotBlank @Length(max = 100) private String gatherName; @NotBlank @Length(max = 10) private String business_type; / @NotBlank @Length(max = 10) private String pay_channel; // .......其他校验 // 过滤 get/set 方法 } 复制代码
在controller
层增加@Validated
注解,效果如下:
个人比较喜欢写在参数的前面,也可以写在方法的上面,比较自由
@Controller @RequestMapping("/test") public class TestController{ @RequestMapping(value = "/test", method = RequestMethod.POST) @ResponseBody public Object create(@Validated @RequestBody Product requestString, BindingResult bindResult) { // 统一处理校验注解的错误信息 Result stringBuilder = dealWithError(bindResult); if (stringBuilder != null){ return stringBuilder; } // 自己的业务处理... return ....; } } 复制代码
第三步:验证注解是否生效
到这一步就可以直接请求接口,在接口处进行断点,如果请求正确会直接进入对应的断点,否则会抛出如下案例所示的异常信息,如果校验不通过,会抛出MethodArgumentNotValidException
或者ConstraintViolationException
异常,下面是案例:
{ "timestamp": "2021-01-01T12:08:49.859+00:00", "status": 400, "error": "Bad Request", "trace": "org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'product' on field 'name': rejected value [null]; codes [NotBlank.product.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.name,name]; arguments []; default message ......", "message": "Validation failed for object='product'. Error count: 1", "errors": [ { "codes": [ "NotBlank.product.name", "NotBlank.name", "NotBlank.java.lang.String", "NotBlank" ], "arguments": [ { "codes": [ "product.name", "name" ], "arguments": null, "defaultMessage": "name", "code": "name" } ], "defaultMessage": "不能为空", "objectName": "product", "field": "name", "rejectedValue": null, "bindingFailure": false, "code": "NotBlank" } ], "path": "//test/valid" } 复制代码
到此,一个对象的注解校验基本实现了,但是我们发现注解校验的方式抛出的异常信息不是十分友好,基本都会配合统一的异常处理来处理请求参数的问题,后文会单独讲如何使用全局异常处理来统一的处理异常信息。下面先了解一下如何自定义注解校验。
自定义注解校验:
如果默认的注解规则无法满足业务需求,这时候validator
提供了自定义注解的形式帮助开发者可以进行自定的规则校验。
第一步:定义自定义注解:
首先第一步是确定自己需要自定义的注解:比如我这里定义了一个检查时间格式的注解
/** * 日期格式校验注解 */ import org.hibernate.validator.constraints.EAN; import org.hibernate.validator.constraints.NotBlank; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; // 注意这里有静态导入 import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.RUNTIME; // javadoc 文档标识 @Documented // 可以注入的类型,字段和参数类型 @Target({PARAMETER, FIELD}) // 运行时生效 @Retention(RUNTIME) // 触发校验的对象 @Constraint(validatedBy = {TimeValidator.class}) public @interface Time { // 必须 String message() default "时间格式校验失败"; // 必须 Class<?>[] groups() default {}; // 必须 Class<? extends Payload>[] payload() default {}; String value = ""; // 下面部分可以忽略 @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented @interface List { Time[] value(); } } 复制代码
对于注解的解释:
@Retention(RUNTIME)
:指定此类型的注释将在运行时通过反射方式可用@Constraint
:指定用于验证元素的验证器@Target
:注解的标识范围,比如这里注解可以是参数或者字段
对应的三个固定参数含义:
message
定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制groups
这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作payload
主要是针对bean的,使用不多。
@Constraint
注解声明约束及其可配置属性,同时在对应的真实注解处理类TimeValidator
里面,在真实注解类里面可以随意的注入需要的bean
(AutoWired
等)
注意上面注意除开value
这个属性之外,其他三个属性message
、groups
、payload
都是必须定义的,否则进行校验的时候,会抛出如下的错误:
HV000074: com.xxx.xxx.valid.annotation.Time contains Constraint annotation, but does not contain a groups parameter. 复制代码
扩展:
HV000074
这个错误是如何来的?:首先我们需要明确一点:javax.validator - JSR303的规范是由Hibernate validate作为标准实现的,也就是说虽然Spring已经为我们进行了适配,但是在校验的时候依然使用的Hibernate Validator,所以我们定义自定义的注解需要按照固定的要求规范:
旧版本的文档:docs.jboss.org/hibernate/v…
较新版本的文档:docs.jboss.org/hibernate/s…
org.hibernate.validator.internal.util.logging
下面的几个包中定义了日志以及异常信息:
private static final String getConstraintWithoutMandatoryParameterException = "HV000074: %2$s contains Constraint annotation, but does not contain a %1$s parameter."; 复制代码
具体的提示信息如下图所示:
org.hibernate.validator.internal.util.logging.Log_$logger
注解定义了如下的异常信息提示
@Message( id = 74, value = "%2$s contains Constraint annotation, but does not contain a %1$s parameter." ) ConstraintDefinitionException getConstraintWithoutMandatoryParameterException(String var1, String var2); 复制代码
注意以下几个点:
- 静态字段和属性无法验证。
- 建议在一个类中坚持使用字段 或属性注释。不建议对字段和随附的getter方法进行注释*,*因为这将导致对该字段进行两次验证。
第二步:定义真实注解处理类:
需要实现接口ConstraintValidator
,泛型的第一个参数为注解类,第二个参数为具体校验对象的类型
下面定义校验时间格式是否正确的一个案例,写的非常粗浅,仅供参考:
public class TimeValidator implements ConstraintValidator<Time, String> { /** * 初始化注解的校验内容 * @param constraintAnnotation */ @Override public void initialize(Time constraintAnnotation) { System.err.println("test" + constraintAnnotation); } /** 具体校验代码 */ @Override public boolean isValid(String value, ConstraintValidatorContext constraintContext) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); simpleDateFormat.setLenient(false); boolean isValid = true; try { simpleDateFormat.parse(value); } catch (Exception e) { isValid = false; } // 如果校验失败,设置自定义错误信息 if ( !isValid ) { constraintContext.disableDefaultConstraintViolation(); constraintContext.buildConstraintViolationWithTemplate( "{com.zxd.interview.valid.annotation." + "Time.message}" ) .addConstraintViolation();//很重要的一步,需要将自定义的信息提示模板加入 } return isValid; } } 复制代码
有的同学可能会好奇对于ConstraintValidator
接口作用,英文进行机翻之后的内容如下:
public interface ConstraintValidator<A extends Annotation, T> { /** 初始化验证器,为isValid(Object, ConstraintValidatorContext)调用做准备。传递给定约束声明的约束注释。 保证在使用此实例进行验证之前调用此方法。 默认的实现是no-op。 */ default void initialize(A constraintAnnotation) { } /** * 实现验证逻辑,值的状态不能被改变。 该方法可以并发访问,实现必须确保线程安全。 @value:被校验的值 @ConstraintValidatorContext 校验上下文 */ boolean isValid(T value, ConstraintValidatorContext context); } 复制代码
注意上面提到的线程安全问题,一定要保证实现的结果是线程安全的,否则校验可能会带来并发的问题。
做完上面这些步骤之后,只需要将注解应用到对应的对象上面,在请求的时候就可以进行对应的参数校验了:
public class Product { @NotBlank private String name; // 自己定义的注解 @Time private String time; // 省略get/set } 复制代码
使用 Validator
校验:
有时候我们想在代码里面手动进行注解校验,JSR-303
也考虑到了这一点,下面介绍一下使用Validator
要如何校验,简单的使用可以使用Validation.buildDefaultValidatorFactory()
获取ValidatorFactory
,通过factory.getValidator()
获取对应的校验器Validator
:
@Override public void doSomething(Product product) { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<Product>> validate = validator.validate(product); Map<Object, Object> objectObjectMap = new HashMap<>(); validate.forEach(item -> { // System.err.println("item = "+ item); String message = item.getMessage(); // System.err.println("message " + message); objectObjectMap.put(item.getPropertyPath(), message); }); objectObjectMap.forEach((k, v) -> { System.err.println("key = " + k + " value = " + v); }); } 复制代码
为了更方便的使用校验,减少重复代码,下面构建一个工具类:
public class ValidateUtils { private static Validator validator = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory() .getValidator(); private static final SmartValidator validatorAdapter = new SpringValidatorAdapter(validator); public static Validator getValidator() { return validator; } private static SmartValidator getValidatorAdapter(Validator validator) { return validatorAdapter; } /** * 校验参数,用于普通参数校验 [未测试!] * * @param */ public static void validateParams(Object... params) { Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params); if (!constraintViolationSet.isEmpty()) { throw new ConstraintViolationException(constraintViolationSet); } } /** * 校验对象 * * @param object * @param groups * @param <T> */ public static <T> void validate(T object, Class<?>... groups) { Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups); if (!constraintViolationSet.isEmpty()) { throw new ConstraintViolationException(constraintViolationSet); } } /** * 校验对象 * 使用与 Spring 集成的校验方式。 * * @param object 待校验对象 * @param groups 待校验的组 * @throws BindException */ public static <T> void validateBySpring(T object, Class<?>... groups) throws BindException { DataBinder dataBinder = getBinder(object); dataBinder.validate((Object[]) groups); if (dataBinder.getBindingResult().hasErrors()) { throw new BindException(dataBinder.getBindingResult()); } } private static <T> DataBinder getBinder(T object) { DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass())); dataBinder.setValidator(getValidatorAdapter(validator)); return dataBinder; } } 复制代码
上面的工具类代码来源于文章:www.jianshu.com/p/2432d0f51…