工作几年了,所以只用了粗浅的资料验证,和其他文章不同的是,每一章都不是一味的。如果您还没有阅读上一篇文章,请单击此处。今天就来介绍一下SpringBoot是如何优雅的集成JSR-303进行参数校验的。说到参数校验,你可能用过,但你真的知道怎么用吗?网上的教程很多,大部分都是简单的介绍。什么是JSR-303?JSR-303是JAVAEE6中的一个子规范,称为BeanValidation。BeanValidation为JavaBean验证定义了相应的元数据模型和API。默认的元数据是JavaAnnotations,可以使用XML覆盖和扩展原有的元数据信息。在应用程序中,通过使用BeanValidation或自己定义的约束,如@NotNull、@Max、@ZipCode,可以保证数据模型(JavaBean)的正确性。约束可以附加到字段、getter方法、类或接口。对于一些特定的要求,用户可以很容易地开发定制的约束。BeanValidation是一个运行时数据验证框架,验证完成后会立即返回验证错误信息。添加对SpringBoot的依赖集成JSR-303只需要添加一个starter,如下:内嵌注解是什么?BeanValidation内嵌了很多注解,足够实际开发使用了。注解如下:注解详情@Null被注解的元素必须为空@NotNull被注解的元素不能为空@AssertTrue被注解的元素必须为真@AssertFalse被注解的元素必须为假@Min(value)被注解的元素必须为假是一个数字,其值必须大于或等于指定的最小值@Max(value)被注释的元素必须是一个数字,其值必须小于或等于指定的最大值@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于或等于指定的最小值@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于或等于指定的最大值@Size(max,min)的大小被注解的元素必须在指定范围内在@Digits(integer,fraction)中,被注解的元素必须是一个数字,其值必须在可接受的范围内@Past被注解的元素必须是过去的日期@Future被注解的元素必须是未来的日期@Pattern(value)被注解的元素必须符合指定的正则表达式。以上是BeanValidation的内嵌注解,但是HibernateValidator在原有的基础上也内嵌了几个注解。下面的注解详情@Email被注解的元素必须是邮箱地址@Length被注解的字符串的大小必须在指定的范围内@NotEmpty被注解的字符串必须是非空的@Range被注解的元素必须在合适的范围内如何使用?参数标定验证分为简单验证、嵌套验证和分组验证。简单验证简单验证就是没有嵌套的属性,约束注解可以直接标注在需要的元素上。如下:@DatapublicclassArticleDTO{@NotNull(message="文章id不能为空")@Min(value=1,message="文章id不能为负")privateIntegerid;@NotBlank(message="文章内容不能为空")privateStringcontent;@NotBlank(message="AuthorIdcannotbeempty")privateStringauthorId;@Future(message="Thesubmissiontimecannotbeinthepast")privateDatesubmitTime;}同一个属性可以指定多个约束,比如@NotNull和@MAX,message属性指定不满足约束时的提示信息。上面的约束标记完成后,为了完成校验,需要在controller层的接口上标记@Valid注解,声明一个BindingResult类型的参数来接收校验的结果。下面简单演示一下添加文章的接口,如下:/***添加文章*/@PostMapping("/add")publicStringadd(@Valid@RequestBodyArticleDTOarticleDTO,BindingResultbindingResult)throwsJsonProcessingException{//如果出现错误消息if(bindingResult.hasErrors()){Mapma??p=newHashMap<>();bindingResult.getFieldErrors().forEach((item)->{Stringmessage=item.getDefaultMessage();Stringfield=item.getField();map.put(field,message);});//返回提示信息returnobjectMapper.writeValueAsString(map);}return"success";}仅仅给属性加上约束注解是不够的,还要需要在接口参数上打上@Valid注解,声明一个BindingResult类型的参数来接收校验结果。举个群验证的栗子:上传文章不需要传递文章ID,但是修改文章需要上传文章ID,同样使用DTO接收参数。这时候约束怎么写呢?这时候我们需要修改这个文章ID是按组验证的。上传文章的接口是一组,不需要@NotNull验证。修改文章的接口是一个组,需要进行@NotNull校验。所有的验证注解都有一个groups属性来指定组,Class[]类型,没有实际意义,只需要定义一个或多个接口来区分即可。@DatapublicclassArticleDTO{/***文章ID只有在修改时才需要检查,所以指定groups为修改组*/@NotNull(message="文章id不能为空",groups=UpdateArticleDTO.class)@Min(value=1,message="文章ID不能为负数",groups=UpdateArticleDTO.class)privateIntegerid;/***文章内容的添加和修改必须验证,分组需要指定两个分组*/@NotBlank(message="文章内容不能为空",groups={AddArticleDTO.class,UpdateArticleDTO.class})privateStringcontent;@NotBlank(message="作者Id不能为空",groups=AddArticleDTO.class)privateStringauthorId;/***提交时间是addmodification和modification都需要验证,所以指定两组*/@Future(message="提交时间不能是过去",groups={AddArticleDTO.class,UpdateArticleDTO.class})privateDatesubmitTime;//修改文章分组publicinterfaceUpdateArticleDTO{}//添加文章分组publicinterfaceAddArticleDTO{}}JSR303的@Valid本身不支持组校验,但是Spring在其基础上提供了注解@Validated来支持组校验。@Validated这个注解值属性指定了需要验证的分组。/***添加文章*@Validated:该注解指定校验的分组信息*/@PostMapping("/add")publicStringadd(@Validated(value=ArticleDTO.AddArticleDTO.class)@RequestBodyArticleDTOarticleDTO,BindingResultbindingResult)throwsJsonProcessingException{//如果有错误提示if(bindingResult.hasErrors()){Mapma??p=newHashMap<>();bindingResult.getFieldErrors().forEach((item)->{Stringmessage=item.getDefaultMessage();Stringfield=item.getField();map.put(field,message);});//返回提示信息returnobjectMapper.writeValueAsString(map);}return"success";}Nestedverification嵌套验证的简单解释意思是一个实体包含另一个实体,这两个或多个实体需要被验证。举个栗子:一篇文章可以有一个或多个分类,作者在提交文章时必须指定文章分类,分类是一个单一的实体,包括分类ID、名称等。大致结构如下:publicclassArticleDTO{...文章的一些属性.....//分类信息privateCategoryDTOcategoryDTO;}这时候需要对文章和分类的属性进行校验,称为嵌套确认。嵌套验证很简单,只需要在嵌套的实体属性上标注@Valid注解,里面的属性也会被验证,否则不会被验证。下面文章分类实体类校验:/***文章分类*/@DatapublicclassCategoryDTO{@NotNull(message="分类ID不能为空")@Min(value=1,message="分类ID不能为负")privateIntegerid;@NotBlank(message="分类名称不能为空")privateStringname;}文章实体类中有一个嵌套的文章分类CategoryDTO属性,需要标记@Valid进行嵌套校验,如下:@DatapublicclassArticleDTO{@NotBlank(message="文章内容不能为空")privateStringcontent;@NotBlank(message="作者Id不能为空")privateStringauthorId;@Future(message="提交时间不能为过去")privateDatesubmitTime;/***@Valid这个注解指定CategoryDTO里面的属性也需要校验*/@Valid@NotNull(message="categorycannotbeempty")privateCategoryDTOcategoryDTO;}Controller层添加文章的接口是和上面一样e,并且需要使用@Valid或者@Validated来注入参数。同时需要定义一个BindingResult参数来接收验证结果。嵌套验证对组查询仍然有效。如果嵌套实体类(如CategoryDTO)中验证的属性与接口中@Validated注解指定的组不同,则不会进行验证。JSR-303对集合的嵌套验证也是可行的。比如List的嵌套验证也需要在属性上打上@Valid注解才能生效,如下:@DatapublicclassArticleDTO{/***@Valid这个注解打在集合上,就会对集合进行验证集合中的每个元素*/@Valid@Size(min=1,message="至少一个类别")@NotNull(message="category不能为空")privateListcategoryDTOS;}总结:嵌套验证只需要为需要验证的元素(单个或集合)添加@Valid注解,接口层需要使用@Valid或@Validated注解注入参数。如何接收验证结果?接收验证结果的方式有很多种,但在实际开发中最好选择一种优雅的方式。下面介绍两种常用的方式。需要在Controller层的各个接口方法参数中指定接收BindingResult的方法,Validator会自动将验证后的信息封装到其中。这也是上面示例中使用的方法。如下:@PostMapping("/add")publicStringadd(@Valid@RequestBodyArticleDTOarticleDTO,BindingResultbindingResult){}这种方法的缺点很明显,每个接口方法参数都要声明,每个方法都要处理验证信息,显然不现实,给向上。该方法还有一个优化的方案:在Controller接口方法执行前,使用AOP处理BindingResult消息提示,但仍然不推荐该方案。全局异常捕获参数会在验证失败时抛出MethodArgumentNotValidException或BindException。这两个异常可以在全局异常处理器中捕获,并返回提示信息或自定义信息给客户端。之前单独写过一篇关于全局异常捕获的文章。不懂的可以全屏阅读try-catch。你不恐慌吗?其他的异常捕获作者这里就不详细贴出来了,只贴参数验证的异常捕获(只是举例,具体的返回信息需要自己封装),如下:@RestControllerAdvicepublicclassExceptionRsHandler{@AutowiredprivateObjectMapperobjectMapper;/***参数验证异常步骤*/@ExceptionHandler(value={MethodArgumentNotValidException.class,BindException.class})publicStringonException(Exceptione)throwsJsonProcessingException{BindingResultbindingResult=null;if(einstanceofMethodArgumentNotValidException){bindingResult=((MethodArgumentValidodArgument)ExpResult){bindingResult=((MethodArgumentValidodArgument)ExpResult);}elseif(einstanceofBindException){bindingResult=((BindException)e).getBindingResult();}MaperrorMap=newHashMap<>(16);bindingResult.getFieldErrors().forEach((fieldError)->errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()));returnobjectMapper.writeValueAsString(errorMap);}}spring-boot-starter-validation有什么作用?这个starter的自动配置类是ValidationAutoConfiguration,最重要的代码是注入一个Validator(验证器)实现类,代码如下:@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@ConditionalOnMissingBean(Validator.class)publicstaticLocalValidatorFactoryBeandefaultValidator(){LocalValidatorFactoryBeanfactoryBean=newLocalValidatorFactoryBean();MessageInterpolatorFactoryinterpolatorFactory=newMessageInterpolatorFactory();factoryBean.setMessageInterpolator(interpolatorFactory.getObject()Validary定义了这个接口进行校验;返回?}fSet>validate(Tobject,Class>...groups);Set>validateProperty(Tobject,StringpropertyName,Class>...groups);Set>validateValue(ClassbeanType,StringpropertyName,Objectvalue,Class>...groups);...这个Validator可以自己定制实现一些大公司不用JSR-303根本没有提供@Valid注解,但是有自己的实现。其实本质就是如何使用Validator接口的实现来自定义验证?虽然在日常开发中内置了Constraint注解s是够用了,但是有时候达不到要求,需要自定义一些验证约束。举个栗子:有这样一个例子,传入的数字必须在枚举值范围内,否则校验不通过。自定义一个验证注解,首先需要自定义一个验证注解,如下:@Documented@Constraint(validatedBy={EnumValuesConstraintValidator.class})@Target({METHOD,FIELD,ANNOTATION_TYPE})@Retention(RUNTIME)@NotNull(message="不能为空")public@interfaceEnumValues{/***提示消息*/Stringmessage()default"传入的值不在范围内";/***grouping*@return*/Class>[]groups()default{};Class[]payload()default{};/***可以传入的值*@return*/int[]values()default{};}根据BeanValidationAPI规范,需要以下三个属性:message:定义消息模板,验证失败时输出约束的严重级别。API本身不使用此属性。除了上面三个必须的属性外,还增加了一个values属性来接收限制范围。下面一行代码被标记在验证注解的头部:@Constraint(validatedBy={EnumValuesConstraintValidator.class})这个@Constraint注解指定了通过哪个验证器进行验证。自定义验证注解可以重用嵌入的注解。比如在@EnumValues注解头标上@NotNull注解,这样@EnumValues就具有了@NotNull的作用。自定义验证器@Constraint注解指定验证器为EnumValuesConstraintValidator,所以需要自定义一个。自定义验证器需要实现ConstraintValidator接口。第一个泛型类型是验证注解,第二个是参数类型。代码如下:/***Validator*/publicclassEnumValuesConstraintValidatorimplementsConstraintValidator{/***存储枚举值*/privateSetints=newHashSet<>();/***初始化方法*@注解forparamenumValues验证*/@Overridepublicvoidinitialize(EnumValuesenumValues){for(intvalue:enumValues.values()){ints.add(value);}}/****@paramvalue入参值*@paramcontext*@return*/@OverridepublicbooleanisValid(Integervalue,ConstraintValidatorContextcontext){//判断是否包含这个值returnints.contains(value);}}如果约束注解需要校验其他数据类型,可以自定义对应数据类型Validator的校验,以及然后在约束注释标头的@Constraint注释中指定其他验证器。演示验证注解和验证器自定义成功后即可使用,如下:@DatapublicclassAuthorDTO{@EnumValues(values={1,2},message="Gendercanonlybepassedin1or2")privateIntegergender;}汇总数据作为客户端和服务器之间的屏障,验证起着重要的作用。希望通过本文,对JSR-303数据校验有一个全面的了解。