Spring Boot 如何优雅的校验参数?

今天介绍一下 Spring Boot 如何优雅的整合JSR-303进行参数校验,说到参数校验可能都用过,但网上的教程大多是简单的介绍,所以我们今天详细看来一下 。

什么是 JSR-303?

JSR-303JAVA EE 6 中的一项子规范,叫做 Bean Validation

Bean ValidationJavaBean 验证定义了相应的元数据模型API。缺省的元数据是Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraintBean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

添加依赖

Spring Boot整合JSR-303只需要添加一个starter即可,如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>

内嵌的注解有哪些?

Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

注解详细信息
@Null被注释的元素必须为 null
@NotNull被注释的元素必须不为 null
@AssertTrue被注释的元素必须为 true
@AssertFalse被注释的元素必须为 false
@Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past被注释的元素必须是一个过去的日期
@Future被注释的元素必须是一个将来的日期
@Pattern(value)被注释的元素必须符合指定的正则表达式

以上是Bean Validation的内嵌的注解,但是Hibernate Validator在原有的基础上也内嵌了几个注解,如下。

注解详细信息
@Email被注释的元素必须是电子邮箱地址
@Length被注释的字符串的大小必须在指定的范围内
@NotEmpty被注释的字符串的必须非空
@Range被注释的元素必须在合适的范围内

如何使用?

参数校验分为简单校验嵌套校验分组校验

简单校验

简单的校验即是没有嵌套属性,直接在需要的元素上标注约束注解即可。如下:

@Data
public class ArticleDTO {@NotNull(message = "文章id不能为空")@Min(value = 1,message = "文章ID不能为负数")private Integer id;@NotBlank(message = "文章内容不能为空")private String content;@NotBlank(message = "作者Id不能为空")private String authorId;@Future(message = "提交时间不能为过去时间")private Date submitTime;
}

同一个属性可以指定多个约束,比如@NotNull@MAX,其中的message属性指定了约束条件不满足时的提示信息。

以上约束标记完成之后,要想完成校验,需要在controller层的接口标注@Valid注解以及声明一个BindingResult类型的参数来接收校验的结果。

下面简单的演示下添加文章的接口,如下:

/*** 添加文章*/@PostMapping("/add")public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException {//如果有错误提示信息if (bindingResult.hasErrors()) {Map<String , String> map = new HashMap<>();bindingResult.getFieldErrors().forEach( (item) -> {String message = item.getDefaultMessage();String field = item.getField();map.put( field , message );} );//返回提示信息return objectMapper.writeValueAsString(map);}return "success";}

仅仅在属性上添加了约束注解还不行,还需在接口参数上标注@Valid注解并且声明一个BindingResult类型的参数来接收校验结果。

分组校验

举个栗子:上传文章不需要传文章ID,但是修改文章需要上传文章ID,并且用的都是同一个DTO接收参数,此时的约束条件该如何写呢?

此时就需要对这个文章ID进行分组校验,上传文章接口是一个分组,不需要执行@NotNull校验,修改文章的接口是一个分组,需要执行@NotNull的校验。

所有的校验注解都有一个groups属性用来指定分组,Class<?>[]类型,没有实际意义,因此只需要定义一个或者多个接口用来区分即可。

@Data
public class ArticleDTO {/*** 文章ID只在修改的时候需要检验,因此指定groups为修改的分组*/@NotNull(message = "文章id不能为空",groups = UpdateArticleDTO.class )@Min(value = 1,message = "文章ID不能为负数",groups = UpdateArticleDTO.class)private Integer id;/*** 文章内容添加和修改都是必须校验的,groups需要指定两个分组*/@NotBlank(message = "文章内容不能为空",groups = {AddArticleDTO.class,UpdateArticleDTO.class})private String content;@NotBlank(message = "作者Id不能为空",groups = AddArticleDTO.class)private String authorId;/*** 提交时间是添加和修改都需要校验的,因此指定groups两个*/@Future(message = "提交时间不能为过去时间",groups = {AddArticleDTO.class,UpdateArticleDTO.class})private Date submitTime;//修改文章的分组public interface UpdateArticleDTO{}//添加文章的分组public interface AddArticleDTO{}}

JSR303本身的@Valid并不支持分组校验,但是Spring在其基础提供了一个注解@Validated支持分组校验。@Validated这个注解value属性指定需要校验的分组。

/*** 添加文章* @Validated:这个注解指定校验的分组信息*/@PostMapping("/add")public String add(@Validated(value = ArticleDTO.AddArticleDTO.class) @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException {//如果有错误提示信息if (bindingResult.hasErrors()) {Map<String , String> map = new HashMap<>();bindingResult.getFieldErrors().forEach( (item) -> {String message = item.getDefaultMessage();String field = item.getField();map.put( field , message );} );//返回提示信息return objectMapper.writeValueAsString(map);}return "success";}

嵌套校验

嵌套校验简单的解释就是一个实体中包含另外一个实体,并且这两个或者多个实体都需要校验。

举个栗子:文章可以有一个或者多个分类,作者在提交文章的时候必须指定文章分类,而分类是单独一个实体,有分类ID名称等等。大致的结构如下:

public class ArticleDTO{...文章的一些属性.....//分类的信息private CategoryDTO categoryDTO;
}

此时文章和分类的属性都需要校验,这种就叫做嵌套校验。

嵌套校验很简单,只需要在嵌套的实体属性标注@Valid注解,则其中的属性也将会得到校验,否则不会校验。

如下文章分类实体类校验

/*** 文章分类*/
@Data
public class CategoryDTO {@NotNull(message = "分类ID不能为空")@Min(value = 1,message = "分类ID不能为负数")private Integer id;@NotBlank(message = "分类名称不能为空")private String name;
}

文章的实体类中有个嵌套的文章分类CategoryDTO属性,需要使用@Valid标注才能嵌套校验,如下:

@Data
public class ArticleDTO {@NotBlank(message = "文章内容不能为空")private String content;@NotBlank(message = "作者Id不能为空")private String authorId;@Future(message = "提交时间不能为过去时间")private Date submitTime;/*** @Valid这个注解指定CategoryDTO中的属性也需要校验*/@Valid@NotNull(message = "分类不能为空")private CategoryDTO categoryDTO;}

Controller层的添加文章的接口同上,需要使用@Valid或者@Validated标注入参,同时需要定义一个BindingResult的参数接收校验结果。

嵌套校验针对分组查询仍然生效,如果嵌套的实体类(比如CategoryDTO)中的校验的属性和接口中@Validated注解指定的分组不同,则不会校验。

JSR-303针对集合的嵌套校验也是可行的,比如List的嵌套校验,同样需要在属性上标注一个@Valid注解才会生效,如下:

@Data
public class ArticleDTO {/*** @Valid这个注解标注在集合上,将会针对集合中每个元素进行校验*/@Valid@Size(min = 1,message = "至少一个分类")@NotNull(message = "分类不能为空")private List<CategoryDTO> categoryDTOS;}

总结:嵌套校验只需要在需要校验的元素(单个或者集合)上添加@Valid注解,接口层需要使用@Valid或者@Validated注解标注入参。

如何接收校验结果?

接收校验的结果的方式很多,不过实际开发中最好选择一个优雅的方式,下面介绍常见的两种方式。

BindingResult 接收

这种方式需要在Controller层的每个接口方法参数中指定,Validator会将校验的信息自动封装到其中。这也是上面例子中一直用的方式。如下:

@PostMapping("/add")public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult){}

这种方式的弊端很明显,每个接口方法参数都要声明,同时每个方法都要处理校验信息,显然不现实,舍弃。

此种方式还有一个优化的方案:使用AOP,在Controller接口方法执行之前处理BindingResult的消息提示,不过这种方案仍然不推荐使用

全局异常捕捉

参数在校验失败的时候会抛出的MethodArgumentNotValidException或者BindException两种异常,可以在全局的异常处理器中捕捉到这两种异常,将提示信息或者自定义信息返回给客户端。

作者这里就不再详细的贴出其他的异常捕获了,仅仅贴一下参数校验的异常捕获(仅仅举个例子,具体的返回信息需要自己封装),如下:

@RestControllerAdvice
public class ExceptionRsHandler {@Autowiredprivate ObjectMapper objectMapper;/*** 参数校验异常步骤*/@ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})public String onException(Exception e) throws JsonProcessingException {BindingResult bindingResult = null;if (e instanceof MethodArgumentNotValidException) {bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();} else if (e instanceof BindException) {bindingResult = ((BindException)e).getBindingResult();}Map<String,String> errorMap = new HashMap<>(16);bindingResult.getFieldErrors().forEach((fieldError)->errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()));return objectMapper.writeValueAsString(errorMap);}}

spring-boot-starter-validation做了什么?

这个启动器的自动配置类是ValidationAutoConfiguration,最重要的代码就是注入了一个Validator(校验器)的实现类,代码如下:

@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)@ConditionalOnMissingBean(Validator.class)public static LocalValidatorFactoryBean defaultValidator() {LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();factoryBean.setMessageInterpolator(interpolatorFactory.getObject());return factoryBean;}

这个有什么用呢?Validator这个接口定义了校验的方法,如下:

<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);<T> Set<ConstraintViolation<T>> validateProperty(T object,String propertyName,Class<?>... groups);<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,String propertyName,Object value,Class<?>... groups);
......

这个Validator可以用来自定义实现自己的校验逻辑,有些大公司完全不用JSR-303提供的@Valid注解,而是有一套自己的实现,其实本质就是利用Validator这个接口的实现。

如何自定义校验?

虽说在日常的开发中内置的约束注解已经够用了,但是仍然有些时候不能满足需求,需要自定义一些校验约束。

举个栗子:有这样一个例子,传入的数字要在列举的值范围中,否则校验失败。

自定义校验注解

首先需要自定义一个校验注解,如下:

@Documented
@Constraint(validatedBy = { EnumValuesConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@NotNull(message = "不能为空")
public @interface EnumValues {/*** 提示消息*/String message() default "传入的值不在范围内";/*** 分组* @return*/Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };/*** 可以传入的值* @return*/int[] values() default { };
}

根据Bean Validation API 规范的要求有如下三个属性是必须的:

  1. message:定义消息模板,校验失败时输出

  2. groups:用于校验分组

  3. payloadBean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。

除了以上三个必须要的属性,添加了一个values属性用来接收限制的范围。

该校验注解头上标注的如下一行代码:

@Constraint(validatedBy = { EnumValuesConstraintValidator.class})

这个@Constraint注解指定了通过哪个校验器去校验。

自定义校验注解可以复用内嵌的注解,比如@EnumValues注解头上标注了一个@NotNull注解,这样@EnumValues就兼具了@NotNull的功能。

自定义校验器

@Constraint注解指定了校验器为EnumValuesConstraintValidator,因此需要自定义一个。

自定义校验器需要实现ConstraintValidator<A extends Annotation, T>这个接口,第一个泛型是校验注解,第二个是参数类型。代码如下:

/*** 校验器*/
public class EnumValuesConstraintValidator implements ConstraintValidator<EnumValues,Integer> {/*** 存储枚举的值*/private  Set<Integer> ints=new HashSet<>();/*** 初始化方法* @param enumValues 校验的注解*/@Overridepublic void initialize(EnumValues enumValues) {for (int value : enumValues.values()) {ints.add(value);}}/**** @param value  入参传的值* @param context* @return*/@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {//判断是否包含这个值return ints.contains(value);}
}

如果约束注解需要对其他数据类型进行校验,则可以的自定义对应数据类型的校验器,然后在约束注解头上的@Constraint注解中指定其他的校验器。

演示

校验注解和校验器自定义成功之后即可使用,如下:

@Data
public class AuthorDTO {@EnumValues(values = {1,2},message = "性别只能传入1或者2")private Integer gender;
}

总结

数据校验作为客户端和服务端的一道屏障,有着重要的作用,通过这篇文章希望能够对JSR-303数据校验有着全面的认识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/543996.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ruby hash方法_Ruby中带有示例的Hash.keys方法

ruby hash方法哈希键方法 (Hash.keys Method) In this article, we will study about Hash.keys Method. The working of the method cant be assumed because of its quite a different name. Let us read its definition and understand its implementation with the help of…

c#组元(Tuple)的使用

组元(Tuple)是C# 4.0引入的一个新特性&#xff0c;可以在.NET Framework 4.0或更高版本中使用。组元使用泛型来简化类的定义&#xff0c;多用于方法的返回值。在函数需要返回多个类型的时候&#xff0c;就不必使用out , ref等关键字了&#xff0c;直接定义一个Tuple类型&#x…

浅谈一下 MyBatis 批量插入的 3 种方法!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone批量插入功能是我们日常工作中比较常见的业务功能之一&#xff0c;今天咱们来一个 MyBatis 批量插入的汇总篇&#xff0c;同时对 3 种实现…

VMware 11安装Mac OS X 10.10 及安装Mac Vmware Tools.

http://www.bubuko.com/infodetail-790015.html#title3 http://my.oschina.net/vigiles/blog/141689#OSC_h2_9 http://www.360doc.com/content/10/0316/11/750552_18965817.shtml转载于:https://www.cnblogs.com/yuyang2100/p/4938829.html

kotlin 查找id_Kotlin程序查找圆柱体区域

kotlin 查找idA cylinder is a three-dimensional structure which has circular bases parallel to each other. 圆柱是具有彼此平行的圆形底的三维结构。 Formula to find area of a cylinder: 2*PI*(radiusheight) 查找圆柱体面积的公式&#xff1a;2 * PI *(半径高度) Gi…

快速搭建 SpringCloud Alibaba Nacos 配置中心!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Cloud Alibaba 是阿里巴巴提供的一站式微服务开发解决方案&#xff0c;目前已被 Spring Cloud 官方收录。而 Nacos 作…

c# datetime._C#| DateTime.TimeOfDay属性(带示例)

c# datetime.DateTime.TimeOfDay属性 (DateTime.TimeOfDay Property) DateTime.TimeOfDay Property is used to get the time of the day of this object. Its a GET property of DateTime class. DateTime.TimeOfDay属性用于获取该对象一天中的时间。 这是DateTime类的GET属性…

JSP 基础(一)

JavaServletPage(JSP) 一 JSP简介 Servlet的缺陷–Servlet的编码、部署和调试任务繁琐–生成动态网页繁琐&#xff0c;不利于项目分工为了弥补Servlet的这些缺陷&#xff0c;SUN公司在Servlet的基础上推出了JSP技术作为解决方案采用JSP技术编写动态页面 –由HTML语句和嵌套在其…

浅聊一下建表的15个小技巧

前言对于后端开发同学来说&#xff0c;访问数据库&#xff0c;是代码中必不可少的一个环节。系统中收集到用户的核心数据&#xff0c;为了安全性&#xff0c;我们一般会存储到数据库&#xff0c;比如&#xff1a;mysql&#xff0c;oracle等。后端开发的日常工作&#xff0c;需要…

c# datetime._C#| 带示例的DateTime.DayOfWeek属性

c# datetime.DateTime.DayOfWeek属性 (DateTime.DayOfWeek Property) DateTime.DayOfWeek Property is used to return the day of the week. DateTime.DayOfWeek属性用于返回星期几。 Syntax: 句法&#xff1a; DayOfWeek DateTime.DayOfWeek;Return value: 返回值&#xff…

JConsole的使用手册 JDK1.5(转)

一篇Sun项目主页上介绍JConsole使用的文章&#xff0c;前段时间性能测试的时候大概翻译了一下以便学习&#xff0c;今天整理一下发上来&#xff0c;有些地方也不知道怎么翻&#xff0c;就保留了原文&#xff0c;可能还好理解点&#xff0c;呵呵&#xff0c;水平有限&#xff0c…

一文快速上手 Nacos 注册中心+配置中心!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Cloud Alibaba 是阿里巴巴提供的一站式微服务开发解决方案&#xff0c;目前已被 Spring Cloud 官方收录。而 Nacos 作…

所有子序列的逆序对总和_一个数字的所有子串的总和

所有子序列的逆序对总和Problem statement: 问题陈述&#xff1a; Given an integer, S represented as a string, get the sum of all possible substrings of this string. 给定一个以字符串形式表示的整数S &#xff0c;得到该字符串所有可能的子字符串的和 。 Input: 输入…

synchronized:使用不规范,老板泪两行!

线程安全问题一直是系统亘古不变的痛点。这不&#xff0c;最近在项目中发了一个错误使用线程同步的案例。表面上看已经使用了同步机制&#xff0c;一切岁月静好&#xff0c;但实际上线程同步却毫无作用。关于线程安全的问题&#xff0c;基本上就是在挖坑与填坑之间博弈&#xf…

SQL --运算符

2019独角兽企业重金招聘Python工程师标准>>> 一、<> (安全等于运算符) mysql中的 、<>或!运算符&#xff0c;相信大家已经很清楚了。今天看到了<>这个运算符&#xff0c;记录下来。 1><>和号的相同点 他们都是两个值比较符&#xff0c;相…

linux 文件浏览器_浏览Linux文件系统

linux 文件浏览器你为什么要学习&#xff1f; (Why would you want to learn?) Linux is probably the most used operating system when it comes to development. For a developer, Linux provides all the required tools. Learning how to navigate the Linux file system…

@Autowired 和 @Resource 的 5 点区别!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Autowired 和 Resource 都是 Spring/Spring Boot 项目中&#xff0c;用来进行依赖注入的注解。它们都提供了将依赖对…

rsync同步数据到内网

最近公司要求将IDC的APP日志备份到公司办公网内部&#xff0c;思前想后&#xff0c;结合以前学过的知识&#xff0c;决定用rsync直接推送&#xff0c;即从APP服务器上直接将日志推送到公司内网。这样避免了在生产服务器上额外安装更多软件而且只需要进行简单的配置&#xff0c;…

sql 更改列数据类型_SQL查询更改列数据类型

sql 更改列数据类型Datatype of the column in SQL can be changed using the ALTER TABLE command. It can also be used to ADD, DELETE or MODIFY columns in already existing tables. It can also be used to ADD or DROP constraints in the SQL database. 可以使用ALTER…

SpringBoot 时间格式化的 5 种实现方法!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在我们日常工作中&#xff0c;时间格式化是一件经常遇到的事儿&#xff0c;所以本文我们就来盘点一下 Spring Boot 中时间格…