Introduction

有参数传递的地方都少不了参数校验。在web开发中,前端的参数校验是为了用户体验,后端的参数校验是为了安全。试想一下,如果在controller层中没有经过任何校验的参数通过service层、dao层一路来到了数据库就可能导致严重的后果,最好的结果是查不出数据,严重一点就是报错,如果这些没有被校验的参数中包含了恶意代码,那就可能导致更严重的后果。

这里我们主要介绍在springboot中的几种参数校验方式。常用的用于参数校验的注解如下:

  • @AssertFalse 所注解的元素必须是Boolean类型,且值为false
  • @AssertTrue 所注解的元素必须是Boolean类型,且值为true
  • @DecimalMax 所注解的元素必须是数字,且值小于等于给定的值
  • @DecimalMin 所注解的元素必须是数字,且值大于等于给定的值
  • @Digits 所注解的元素必须是数字,且值必须是指定的位数
  • @Future 所注解的元素必须是将来某个日期
  • @Max 所注解的元素必须是数字,且值小于等于给定的值
  • @Min 所注解的元素必须是数字,且值小于等于给定的值
  • @Range 所注解的元素需在指定范围区间内
  • @NotNull 所注解的元素值不能为null
  • @NotBlank 所注解的元素值有内容
  • @Null 所注解的元素值为null
  • @Past 所注解的元素必须是某个过去的日期
  • @PastOrPresent 所注解的元素必须是过去某个或现在日期
  • @Pattern 所注解的元素必须满足给定的正则表达式
  • @Size 所注解的元素必须是String、集合或数组,且长度大小需保证在给定范围之内
  • @Email 所注解的元素需满足Email格式

controller层参数校验

在controller层的参数校验可以分为两种场景:

  1. 单个参数校验
  2. 实体类参数校验

单个参数校验

@RestController
@Validated
public class PingController { @GetMapping("/getUser")
public String getUserStr(@NotNull(message = "name 不能为空") String name,
@Max(value = 99, message = "不能大于99岁") Integer age) {
return "name: " + name + " ,age:" + age;
}
}

当处理GET请求时或只传入少量参数的时候,我们可能不会建一个bean来接收这些参数,就可以像上面这样直接在controller方法的参数中进行校验。

注意:这里一定要在方法所在的controller类上加入@Validated注解,不然没有任何效果。

这时候在postman输入请求:

http://localhost:8080/getUser?name=Allan&age=101

调用方会收到springboot默认的格式报错:

{
"timestamp": "2019-06-01T04:30:26.882+0000",
"status": 500,
"error": "Internal Server Error",
"message": "getUserStr.age: 不能大于99岁",
"path": "/getUser"
}

后台会打印如下错误:

javax.validation.ConstraintViolationException: getUserStr.age: 不能大于99岁
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at io.shopee.bigdata.penalty.server.controller.PingController$$EnhancerBySpringCGLIB$$232cfd51.getUserStr(<generated>)
...

如果有很多使用这种参数验证的controller方法,我们希望在一个地方对ConstraintViolationException异常进行统一处理,可以使用统一异常捕获,这需要借助@ControllerAdvice注解来实现,当然在springboot中我们就用@RestControllerAdvice(内部包含@ControllerAdvice和@ResponseBody的特性)

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.Set; /**
* @author pengchengbai
* @date 2019-06-01 14:09
*/
@RestControllerAdvice
public class GlobalExceptionHandler { @ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ValidationException exception) {
if(exception instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) exception; Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
//打印验证不通过的信息
System.out.println(item.getMessage());
}
}
return "bad request" ;
}
}

当参数校验异常的时候,该统一异常处理类在控制台打印信息的同时把bad request的字符串和HttpStatus.BAD_REQUEST所表示的状态码400返回给调用方(用@ResponseBody注解实现,表示该方法的返回结果直接写入HTTP response body 中)。其中:

  • @ControllerAdvice:控制器增强,使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到所有的 @RequestMapping注解的方法。
  • @ExceptionHandler:异常处理器,此注解的作用是当出现其定义的异常时进行处理的方法,此例中处理ValidationException异常。

实体类参数校验

当处理post请求或者请求参数较多的时候我们一般会选择使用一个bean来接收参数,然后在每个需要校验的属性上使用参数校验注解:

@Data
public class UserInfo {
@NotNull(message = "username cannot be null")
private String name; @NotNull(message = "sex cannot be null")
private String sex; @Max(value = 99L)
private Integer age;
}

然后在controller方法中用@RequestBody表示这个参数接收的类:

@RestController
public class PingController {
@Autowired
private Validator validator; @GetMapping("metrics/ping")
public Response<String> ping() {
return new Response<>(ResponseCode.SUCCESS, null,"pang");
} @PostMapping("/getUser")
public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
validData(bindingResult); return "name: " + user.getName() + ", age:" + user.getAge();
} private void validData(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
StringBuffer sb = new StringBuffer();
for (ObjectError error : bindingResult.getAllErrors()) {
sb.append(error.getDefaultMessage());
}
throw new ValidationException(sb.toString());
}
}
}

需要注意的是,如果想让UserInfo中的参数注解生效,还必须在Controller参数中使用@Validated注解。这种参数校验方式的校验结果会被放到BindingResult中,我们这里写了一个统一的方法来处理这些结果,通过抛出异常的方式得到GlobalExceptionHandler的统一处理。

校验模式

在上面的例子中,我们使用BindingResult验证不通过的结果集合,但是通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。这就涉及到两种校验模式的配置:

  1. 普通模式(默认是这个模式): 会校验完所有的属性,然后返回所有的验证失败信息
  2. 快速失败模式: 只要有一个验证失败,则返回

    如果想要配置第二种模式,需要添加如下配置类:
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory; @Configuration
public class ValidatorConf {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator(); return validator;
}
}

参数校验分组

在实际开发中经常会遇到这种情况:想要用一个实体类去接收多个controller的参数,但是不同controller所需要的参数又有些许不同,而你又不想为这点不同去建个新的类接收参数。比如有一个/setUser接口不需要id参数,而/getUser接口又需要该参数,这种时候就可以使用参数分组来实现。

  1. 定义表示组别的interface
public interface GroupA {
}
  1. @Validated中指定使用哪个组;
@RestController
public class PingController {
@PostMapping("/getUser")
public String getUserStr(@RequestBody @Validated({GroupA.class, Default.class}) UserInfo user, BindingResult bindingResult) {
validData(bindingResult);
return "name: " + user.getName() + ", age:" + user.getAge();
} @PostMapping("/setUser")
public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
validData(bindingResult);
return "name: " + user.getName() + ", age:" + user.getAge();
}

其中Defaultjavax.validation.groups中的类,表示参数类中其他没有分组的参数,如果没有,/getUser接口的参数校验就只会有标记了GroupA的参数校验生效。

  1. 在实体类的注解中标记这个哪个组所使用的参数;
@Data
public class UserInfo {
@NotNull( groups = {GroupA.class}, message = "id cannot be null")
private Integer id; @NotNull(message = "username cannot be null")
private String name; @NotNull(message = "sex cannot be null")
private String sex; @Max(value = 99L)
private Integer age;
}

级联参数校验

当参数bean中的属性又是一个复杂数据类型或者是一个集合的时候,如果需要对其进行进一步的校验需要考虑哪些情况呢?

@Data
public class UserInfo {
@NotNull( groups = {GroupA.class}, message = "id cannot be null")
private Integer id; @NotNull(message = "username cannot be null")
private String name; @NotNull(message = "sex cannot be null")
private String sex; @Max(value = 99L)
private Integer age; @NotEmpty
private List<Parent> parents;
}

比如对于parents参数,@NotEmpty只能保证list不为空,但是list中的元素是否为空、User对象中的属性是否合格,还需要进一步的校验。这个时候我们可以这样写:

    @NotEmpty
private List<@NotNull @Valid UserInfo> parents;

然后再继续在UserInfo类中使用注解对每个参数进行校验。

但是我们再回过头来看看,在controller中对实体类进行校验的时候使用的@Validated,在这里只能使用@Valid,否则会报错。关于这两个注解的具体区别可以参考@Valid 和@Validated的关系,但是在这里我想说的是使用@Valid就没办法对UserInfo进行分组校验。这种时候我们就会想,如果能够定义自己的validator就好了,最好能支持分组,像函数一样调用对目标参数进行校验,就像下面的validObject方法一样:

import javax.validation.Validator;

@RestController
public class PingController {
@Autowired
private Validator validator; @PostMapping("/setUser")
public String setUser(@RequestBody @Validated UserInfo user, BindingResult bindingResult) {
validData(bindingResult);
Parent parent = user.getParent();
validObject(parent, validator, GroupB.class, Default.class);
return "name: " + user.getName() + ", age:" + user.getAge();
} private void validData(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
StringBuffer sb = new StringBuffer();
for (ObjectError error : bindingResult.getAllErrors()) {
sb.append(error.getDefaultMessage());
}
throw new ValidationException(sb.toString());
}
} /**
* 实体类参数有效性验证
* @param bean 验证的实体对象
* @param groups 验证组
* @return 验证成功:返回true;验证失败:将错误信息添加到message中
*/
public void validObject(Object bean, Validator validator, Class<?> ...groups) {
Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(bean, groups);
if (!constraintViolationSet.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation violation: constraintViolationSet) {
sb.append(violation.getMessage());
} throw new ValidationException(sb.toString());
}
}
} @Data
public class Parent {
@NotEmpty(message = "parent name cannot be empty", groups = {GroupB.class})
private String name; @Email(message = "should be email format")
private String email;
}

自定义参数校验

虽然JSR303和Hibernate Validtor 已经提供了很多校验注解,但是当面对复杂参数校验时,还是不能满足我们的要求,这时候我们就需要自定义校验注解。这里我们再回到上面的例子介绍一下自定义参数校验的步骤。private List<@NotNull @Valid UserInfo> parents这种在容器中进行参数校验是Bean Validation2.0的新特性,假如没有这个特性,我们来试着自定义一个List数组中不能含有null元素的注解。这个过程大概可以分为两步:

  1. 自定义一个用于参数校验的注解,并为该注解指定校验规则的实现类
  2. 实现校验规则的实现类

自定义注解

定义@ListNotHasNull注解, 用于校验 List 集合中是否有null 元素

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//此处指定了注解的实现类为ListNotHasNullValidatorImpl
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)
public @interface ListNotHasNull { /**
* 添加value属性,可以作为校验时的条件,若不需要,可去掉此处定义
*/
int value() default 0; String message() default "List集合中不能含有null元素"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; /**
* 定义List,为了让Bean的一个属性上可以添加多套规则
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@interface List {
ListNotHasNull[] value();
}
}

注意:message、groups、payload属性都需要定义在参数校验注解中不能缺省

注解实现类

该类需要实现ConstraintValidator

import org.springframework.stereotype.Service;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List; public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> { private int value; @Override
public void initialize(ListNotHasNull constraintAnnotation) {
//传入value 值,可以在校验中使用
this.value = constraintAnnotation.value();
} public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
for (Object object : list) {
if (object == null) {
//如果List集合中含有Null元素,校验失败
return false;
}
}
return true;
}
}

然后我们就能在之前的例子中使用该注解了:

@NotEmpty
@ListNotHasNull
private List<@Valid UserInfo> parents;

其他

Difference Between @NotNull, @NotEmpty, and @NotBlank

@NotNull

不能为null,但是可以为空字符串""

@NotEmpty

不能为null,不能为空字符串"",其本质是CharSequence, Collection, Map, or Array的size或者length不能为0

@NotBlank

a constrained String is valid as long as it’s not null and the trimmed length is greater than zero

@NonNull

@NotNull 是 JSR303(Bean的校验框架)的注解,用于运行时检查一个属性是否为空,如果为空则不合法。

@NonNull 是JSR 305(缺陷检查框架)的注解,是告诉编译器这个域不可能为空,当代码检查有空值时会给出一个风险警告,目前这个注解只有IDEA支持。

@Valid 注解和描述

参考资料

  1. spring5.0 中的@NonNull
  2. Difference Between @NotNull, @NotEmpty, and @NotBlank
  3. https://my.oschina.net/u/3773384/blog/1795869
  4. @Valid 和@Validated的关系
  5. 校验bean中的bean
  6. springboot使用hibernate validator校验
  7. 使用hibernate validation完成数据后端校验

SpringBoot 参数校验的方法的更多相关文章

  1. springmvc、springboot 参数校验

    参数校验在项目中是必不可少的,不仅前端需要校验,为了程序的可靠性,后端也需要对参数进行有效性的校验.下面将介绍在springmvc或springboot项目中参数校验的方法 准备工作: 引入校验需要用 ...

  2. 补习系列(4)-springboot 参数校验详解

    目录 目标 一.PathVariable 校验 二.方法参数校验 三.表单对象校验 四.RequestBody 校验 五.自定义校验规则 六.异常拦截器 参考文档 目标 对于几种常见的入参方式,了解如 ...

  3. springboot 参数校验详解

    https://www.jianshu.com/p/89a675b7c900 在日常开发写rest接口时,接口参数校验这一部分是必须的,但是如果全部用代码去做,显得十分麻烦,spring也提供了这部分 ...

  4. SpringBoot 参数校验

    一.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...

  5. springboot参数校验

    为了能够进行嵌套验证,必须手动在Item实体的props字段上明确指出这个字段里面的实体也要进行验证.由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且 ...

  6. springboot 接口参数校验

    前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...

  7. 【快学springboot】4.接口参数校验

    前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...

  8. SpringBoot 如何进行参数校验,老鸟们都这么玩的!

    大家好,我是飘渺. 前几天写了一篇 SpringBoot如何统一后端返回格式?老鸟们都是这样玩的! 阅读效果还不错,而且被很多号主都转载过,今天我们继续第二篇,来聊聊在SprinBoot中如何集成参数 ...

  9. Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    之前也写过一篇关于Spring Validation使用的文章,不过自我感觉还是浮于表面,本次打算彻底搞懂Spring Validation.本文会详细介绍Spring Validation各种场景下 ...

随机推荐

  1. .net core web api部署到docker

    一.创建.net core web api 的Demo 修改部分代码 端口随意指定,ip用星号“*”,方便接下来docker虚拟网络自动分配ip 下一步是Dockerfile文件,如果发现你的项目中没 ...

  2. 实验:keepalived双主抢占模式和非抢占模式和IPVS

    内容: 一:概念.原理   二:实验过程 一.概念 一.keepalived原理及配置解析 keepalived:vrrp协议的实现 vrrp协议:virtual router redundancy ...

  3. 从Dictionary源码看哈希表

    一.基本概念 哈希:哈希是一种查找算法,在关键字和元素的存储地址之间建立一个确定的对应关系,每个关键字对应唯一的存储地址,这些存储地址构成了有限.连续的存储地址. 哈希函数:在关键字和元素的存储地址之 ...

  4. golang学习(1)---快速hello world

    很多著名的计算机语言都是一两个人在业余时间捣鼓出来的,但是Go语言是由Google的团队打造的.可能一些基础的知识点我不会细讲,因为这个时代你真的得快速学习,才能适应发展. 来看看go的hello, ...

  5. html5标签整理

    html元素 基础标签 <!DOCTYPE> 定义文档类型(e.g  <!DOCTYPE  html>) <html>定义一个HTML文档</html> ...

  6. 十分钟搭建微服务框架(SpringBoot +Dubbo+Docker+Jenkins源码)

    本文将以原理+实战的方式,首先对“微服务”相关的概念进行知识点扫盲,然后开始手把手教你搭建这一整套的微服务系统. 这套微服务框架能干啥? 这套系统搭建完之后,那可就厉害了: 微服务架构 你的整个应用程 ...

  7. jmeter学习笔记-----第一天

    环境简介:apache-jmeter-3.3    win10系统 一.Jmeter录制操作步骤: 1.工作台---添加HTTP代理服务器: 2.为自己笔记本的浏览器设置相同代理: 3.测试计划下-s ...

  8. 【畅通工程 HDU - 1232 】【并查集模板】

    并查集讲解和模板 有一个博文对此分析的很透彻,附链接 为避免原链接失效,现摘录如下: 为了解释并查集的原理,我将举一个更有爱的例子. 话说江湖上散落着各式各样的大侠,有上千个之多.他们没有什么正当职业 ...

  9. 如何让springboot打包的项目部署在阿里云上使用https和http方式访问

    前言 问题描述:怎么让springboot部署在服务器上使用https协议方式访问我们的接口或者域名,目的是某些平台请求的是https协议,而不是https 部署环境:阿里云 centos7服务器,s ...

  10. Linux权限管理(7)

    权限的基本介绍: rwx权限详解: rwx作用到文件: [r]:代表可读,可以读取.查看 [w]:代表可写,可以修改,但不代表可以删除该文件,删除一个文件的前提条件是对该文件所在的目录有写权限才能删除 ...