Spring Boot 使用 JSR303 实现参数验证
简介
JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。
在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的。在通常的情况下,应用程序是分层的,不同的层由不同的开发人员来完成。很多时候同样的数据验证逻辑会出现在不同的层,这样就会导致代码冗余和一些管理的问题,比如说语义的一致性等。为了避免这样的情况发生,最好是将验证逻辑与相应的域模型进行绑定。
Bean Validation 为 JavaBean 验证定义了相应的元数据模型和 API。缺省的元数据是 Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用 Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode, 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。
Bean Validation 规范内嵌的约束注解
、
实例
基本应用
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
给参数对象添加校验注解
@Data
public class User {
private Integer id;
@NotBlank(message = "用户名不能为空")
private String username;
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密码必须为8~16个字母和数字组合")
private String password;
@Email
private String email;
private Integer gender;
}
Controller 中需要校验的参数Bean前添加 @Valid 开启校验功能,紧跟在校验的Bean后添加一个BindingResult,BindingResult封装了前面Bean的校验结果。
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("")
public Result save (@Valid User user , BindingResult bindingResult) {
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 Result.build( 400 , "非法参数 !" , map);
}
return Result.ok();
}
}
测试如下:

异常的统一处理
参数校验不通过时,会抛出 BingBindException 异常,可以在统一异常处理中,做统一处理,这样就不用在每个需要参数校验的地方都用 BindingResult 获取校验结果了。
@Slf4j
@RestControllerAdvice(basePackages = "com.itwolfed.controller")
public class GlobalExceptionControllerAdvice {
@ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
public Result handleVaildException(Exception e){
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 Result.build(400 , "非法参数 !" , errorMap);
}
}
分组解决校验
新增和修改对于实体的校验规则是不同的,例如id是自增的时,新增时id要为空,修改则必须不为空;新增和修改,若用的恰好又是同一种实体,那就需要用到分组校验。
校验注解都有一个groups属性,可以将校验注解分组,我们看下@NotNull的源码:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {
String message() default "{javax.validation.constraints.NotNull.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@interface List {
NotNull[] value();
}
}
从源码可以看出 groups 是一个Class<?>类型的数组,那么就可以创建一个Groups.
public class Groups {
public interface Add{}
public interface Update{}
}
给参数对象的校验注解添加分组
@Data
public class User {
@Null(message = "新增不需要指定id" , groups = Groups.Add.class)
@NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
private Integer id;
@NotBlank(message = "用户名不能为空")
@NotNull
private String username;
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密码必须为8~16个字母和数字组合")
private String password;
@Email
private String email;
private Integer gender;
}
Controller 中原先的@Valid不能指定分组 ,需要替换成@Validated
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("")
public Result save (@Validated(Groups.Add.class) User user) {
return Result.ok();
}
}
测试如下:

自定义校验注解
虽然JSR303和springboot-validator 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要 自定义校验注解。
例如User中的gender,用 1代表男 2代表女,我们自定义一个校验注解@ListValue,指定取值只能1和2。
创建约束规则
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
一个标注(annotation) 是通过@interface关键字来定义的. 这个标注中的属性是声明成类似方法
的样式的. 根据Bean Validation API 规范的要求:
- message属性, 这个属性被用来定义默认得消息模版, 当这个约束条件被验证失败的时候,通过
此属性来输出错误信息。 - groups 属性, 用于指定这个约束条件属于哪(些)个校验组. 这个的默认值必须是Class<?>类型数组。
- payload 属性, Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。
除了这三个强制性要求的属性(message, groups 和 payload) 之外, 我们还添
加了一个属性用来指定所要求的值. 此属性的名称vals在annotation的定义中比较特
殊, 如果只有这个属性被赋值了的话, 那么, 在使用此annotation到时候可以忽略此属性名称.
另外, 我们还给这个annotation标注了一些元标注( meta
annotatioins):
- @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示此注解可以被用在方法, 字段或者
annotation声明上。 - @Retention(RUNTIME): 表示这个标注信息是在运行期通过反射被读取的.
- @Constraint(validatedBy = ListValueConstraintValidator.class): 指明使用哪个校验器(类) 去校验使用了此标注的元素.
- @Documented: 表示在对使用了该注解的类进行javadoc操作到时候, 这个标注会被添加到
javadoc当中.
创建约束校验器
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
/**
* 初始化方法
*/
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 判断是否校验成功
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
ListValueConstraintValidator定义了两个泛型参数, 第一个是这个校验器所服务到标注类型(在我们的例子中即ListValue), 第二个这个校验器所支持到被校验元素的类型 (即Integer)。
如果一个约束标注支持多种类型的被校验元素的话, 那么需要为每个所支持的类型定义一个ConstraintValidator,并且注册到约束标注中。
这个验证器的实现就很平常了, initialize() 方法传进来一个所要验证的标注类型的实例, 在本
例中, 我们通过此实例来获取其vals属性的值,并将其保存为Set集合中供下一步使
用。
isValid()是实现真正的校验逻辑的地方, 判断一个给定的int对于@ListValue这个约束条件来说
是否是合法的。
在参数对象中使用@ListValue注解。
@Data
public class User {
@Null(message = "新增不需要指定id" , groups = Groups.Add.class)
@NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
private Integer id;
@NotBlank(message = "用户名不能为空")
@NotNull
private String username;
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密码必须为8~16个字母和数字组合")
private String password;
@Email
private String email;
@ListValue( message = "性别应指定相应的值" , vals = {1,2} , groups = {Groups.Add.class , Groups.Update.class})
private Integer gender;
}
测试如下:

源码地址
https://github.com/gf-huanchupk/SpringBootLearning
参考
https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html
https://docs.jboss.org/hibernate/validator/4.3/reference/zh-CN/pdf/hibernate_validator_reference.pdf
欢迎扫码或微信搜索公众号《程序员果果》关注我,关注有惊喜~

Spring Boot 使用 JSR303 实现参数验证的更多相关文章
- Spring Boot 之:接口参数校验
Spring Boot 之:接口参数校验,学习资料 网址 SpringBoot(八) JSR-303 数据验证(写的比较好) https://qq343509740.gitee.io/2018/07/ ...
- 【转】spring boot application.properties 配置参数详情
multipart multipart.enabled 开启上传支持(默认:true) multipart.file-size-threshold: 大于该值的文件会被写到磁盘上 multipart. ...
- Spring Boot获取前端页面参数的几种方式总结
Spring Boot的一个好处就是通过注解可以轻松获取前端页面的参数,之后可以将参数经过一系列处理传送到后台数据库. 获得的方式有很多种,这里稍微总结一下,大致分为以下几种: 1.指定前端url请求 ...
- spring boot mapper层传参数是用main的arg0(第一个参数),arg1(第二个参数)
spring boot mapper层传参数是用main的arg0(第一个参数),arg1(第二个参数) 大于三个参数,用map传递 public interface FrontMapper{ //= ...
- 探究Spring Boot中的接收参数问题与客户端发送请求传递数据
结合此篇参考Spring框架学习笔记(9)--API接口设计相关知识及具体编码实现 在使用Spring Boot进行接收参数的时候,发现了许多问题,之前一直都很忙,最近才稍微有空研究一下此问题. 网上 ...
- 玩转spring boot——AOP与表单验证
AOP在大多数的情况下的应用场景是:日志和验证.至于AOP的理论知识我就不做赘述.而AOP的通知类型有好几种,今天的例子我只选一个有代表意义的“环绕通知”来演示. 一.AOP入门 修改“pom.xml ...
- Spring Boot 系列教程19-后台验证-Hibernate Validation
后台验证 开发项目过程中,后台在很多地方需要进行校验操作,比如:前台表单提交,调用系统接口,数据传输等.而现在多数项目都采用MVC分层式设计,每层都需要进行相应地校验. 针对这个问题, JCP 出台一 ...
- Spring boot中自定义Json参数解析器
转载请注明出处... 一.介绍 用过springMVC/spring boot的都清楚,在controller层接受参数,常用的都是两种接受方式,如下 /** * 请求路径 http://127.0. ...
- spring boot 设置tomcat post参数限制
今天传图片,用的base64字符串,POST方法,前端传送的时候总是莫名其妙的崩溃,去网上搜了半天,以为是文件大小被限制了,但是我这个是字符串接收,不是文件接收,于是又继续搜,原来post本身没有参数 ...
随机推荐
- 一篇文章掌握网页解析神器——xpath
学爬虫不会xpath解析数据? 今天老师带你一堂课带你从零开始精通xpath,从此轻松提取网页信息. 我们在爬虫的过程中,经常会遇到html字符串数据,很多我们需要的数据不是作为标签的文本就是作标签的 ...
- Python selenium Chrome正在受到自动软件的控制 disable-infobars无效 的解决方法
问题解决 前两天更新了google浏览器版本,今天运行以前的脚本,发现options一个参数的配置不生效了. 运行了几次都发现该参数没有生效,也检查了自己的代码参数,没有写错,于是就有了这一波“网中寻 ...
- ansible的基础概念与部署(一)
- 2019-2020-1 20199329《Linux内核原理与分析》第九周作业
<Linux内核原理与分析>第九周作业 一.本周内容概述: 阐释linux操作系统的整体构架 理解linux系统的一般执行过程和进程调度的时机 理解linux系统的中断和进程上下文切换 二 ...
- Scala教程之:面向对象的scala
文章目录 面向对象的scala Unified Types Classes Traits 面向对象的scala 我们知道Scala是一种JVM语言,可以合java无缝衔接,这也就大大的扩展了scala ...
- [转]探索 Android 内存优化方法
前言 这篇文章的内容是我回顾和再学习 Android 内存优化的过程中整理出来的,整理的目的是让我自己对 Android 内存优化相关知识的认识更全面一些,分享的目的是希望大家也能从这些知识中得到一些 ...
- Spark学习笔记(一)
概念: Spark是加州大学伯克利分校AMP实验室,开发的通用内存并行计算框架. 支持用scala.java和Python等语言编写应用程序.相较于Hdoop,往往有更好的运行效率. Spark包括了 ...
- ip地址与运算 ipcalc命令
http://man.linuxde.net/ipcalc 转载于:https://blog.51cto.com/sonlich/2064133
- Codeforce 263D Cycle in Graph 搜索 图论 哈密尔顿环
You've got a undirected graph G, consisting of n nodes. We will consider the nodes of the graph inde ...
- Python安装第三方模块出错 No module named setuptools
在安装 zabbix-alerta 第三方模块时候报错 python setup.py install 此时需要安装 setuptools 模块, 这里用自动化脚本安装 wget https://bo ...