参数校验
在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:

验证代码繁琐,重复劳动
方法内代码显得冗长
每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码
你看这样?我感觉不行 ~有啥好办法不

public String test1(String name) {
if (name == null) {
throw new NullPointerException("name 不能为空");
}
if (name.length() < 2 || name.length() > 10) {
throw new RuntimeException("name 长度必须在 2 - 10 之间");
}
return "success";
}
1
2
3
4
5
6
7
8
9
使用hibernate-validator
spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernate validator依赖。在 pom.xml 中添加上 spring-boot-starter-web 的依赖即可

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1
2
3
4
创建如下实体

@Data
public class Book {
private Integer id;
@NotBlank(message = "name 不允许为空")
@Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
private String name;
}
1
2
3
4
5
6
7
实体校验
然后呢在 controller 中这样写即可验证

验证加@RequestBody 的参数

@RequestMapping("/test")
public String test(@Validated @RequestBody Book book) {
return "success";
}
1
2
3
4
这时候呢会出现MethodArgumentNotValidException异常可以在ControllerAdvice 做全局异常处理

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("请求参数不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof MethodArgumentNotValidException) {
modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}

private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果不加呢?

@RequestMapping("/test")
public String test(@Validated Book book) {
return "success";
}
1
2
3
4
则会出BindException 异常,则又可以在ControllerAdvice 中加入判断

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("请求参数不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof BindException) {
modelMap.put("message", getErrors(((BindException) e).getBindingResult()));
} else if (e instanceof MethodArgumentNotValidException) {
modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
验证参数
如果是get请求参数呢?

@RequestMapping("/test")
public String test(@Validated @NotBlank(message = "name 不允许为空") String name) {
System.out.println("111");
return "success";
}
1
2
3
4
5
我们发现这样根本不好使,其实呢这种需要在类上加入

@Validated
@RestController
public class TestController {
@RequestMapping("/test")
public String test(@NotBlank(message = "name 不允许为空") String name) {
System.out.println("111");
return "success";
}
}
1
2
3
4
5
6
7
8
9
这样才可以生效,此时呢返回ConstraintViolationException 异常可以在全局异常中这样处理

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("请求参数不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof HttpMediaTypeException) {
modelMap.put("message", "请求体不对");
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException exs = (ConstraintViolationException) e;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
modelMap.put("message", getErrors(violations));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}
private Map<String, String> getErrors(Set<ConstraintViolation<?>> violations) {
Map<String, String> map = new HashMap<>();
for (ConstraintViolation<?> item : violations) {
map.put(item.getPropertyPath().toString(), item.getMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Model校验
如过不是想验证传参呢?就是想验证一个实体怎么玩呢?

这样就可以解决了

@RestController
public class TestController {
@Autowired
private Validator validator;
@RequestMapping("/test")
public Map<String, String> test() {
Book book = new Book();
book.setId(1).setName("");
Set<ConstraintViolation<Book>> violationSet = validator.validate(book);
return getErrors(violationSet);
}
private <T> Map<String, String> getErrors(Set<ConstraintViolation<T>> violations) {
Map<String, String> map = new HashMap<>();
for (ConstraintViolation<?> item : violations) {
map.put(item.getPropertyPath().toString(), item.getMessage());
}
return map;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对象级联校验
在比如book那个实体中加入了一个具有对象这时候改怎么办呢?

只需要在实体上加入@Valid 即可

@Data
@Accessors(chain = true)
public class Book {

private Integer id;
@NotBlank(message = "name 不允许为空")
@Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间")
private String name;
@Valid
private Author author;

@Data
@Accessors(chain = true)
public static class Author {
@NotBlank(message = "Author.name 不允许为空")
private String name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hibernate-validator 的校验模式
这时候要说点东西了

上面例子中一次性返回了所有验证不通过的集合,通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。Hibernate Validator有以下两种验证模式

普通模式(默认是这个模式)
普通模式(会校验完所有的属性,然后返回所有的验证失败信息)

快速失败返回模式
快速失败返回模式(只要有一个验证失败,则返回)

​ true 快速失败返回模式 false普通模式

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
1
2
3
4
5
或者这样配

ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
1
2
3
4
5
这样配置就行了

@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
/**设置validator模式为快速失败返回*/
postProcessor.setValidator(validator());
return postProcessor;
}

@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分组校验
分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。

有这样一种场景,新增的时候,不需要验证Id(因为系统生成);修改的时候需要验证Id,这时候可用用户到validator的分组验证功能。

设置validator为普通验证模式("hibernate.validator.fail_fast", "false"),用到的验证GroupA、GroupB和实体:

GroupA、GroupB
1
public interface GroupA {
}
public interface GroupB {
}
1
2
3
4
然后改造一下Book实体

@Data
@Accessors(chain = true)
public class Book {
@NotBlank
@Range(min = 1, max = Integer.MAX_VALUE, message = "必须大于0", groups = {GroupA.class})
private Integer id;
@NotBlank(message = "name 不允许为空")
@Length(min = 4, max = 20, message = "name 长度必须在 {min} - {max} 之间", groups = {GroupB.class})
private String name;
@NotBlank
@Range(min = 0, max = 100, message = "年龄必须在[0,100]", groups = {Default.class})
private Integer age;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GroupA验证字段id;
GroupB验证字段name;
Default验证字段age(Default是Validator自带的默认分组)
这样去验证

@RequestMapping("/test")
public void test() {
Book book = new Book();
/**GroupA验证不通过*/
book.setId(-10086);
/**GroupA验证通过*/
//book.setId(10010);
book.setName("a");
book.setAge(110);
Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupA.class, GroupB.class);
for (ConstraintViolation<Book> item : validate) {
System.out.println(item);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
或者这样

@RequestMapping("/test")
public void test(@Validated({GroupA.class, GroupB.class}) Book book, BindingResult result) {
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
1
2
3
4
5
6
7
8
9
当然这样验证务必要给 组一个序列,不然不行的还是无法实现

@GroupSequence({GroupA.class, GroupB.class, Default.class})
public interface GroupOrder {
}
1
2
3
这样就好了然后这样玩

Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupOrder.class);
1
@Validated({GroupOrder.class})Book book, BindingResult result
1
注意项
如果不想全局拦截异常想看到直观的错误可以在方法参数中加入BindingResult result

单一的可以这样玩

public void test()(@Validated DemoModel demo, BindingResult result)
1
验证多个的话可以这样玩

public void test()(@Validated DemoModel demo, BindingResult result,@Validated DemoModel demo2, BindingResult result2)
1
自定义验证器
一般情况,自定义验证可以解决很多问题。但也有无法满足情况的时候,此时,我们可以实现validator的接口,自定义自己需要的验证器。

首先呢定义个注解,在注解上加入注解@Constraint 绑定验证类

@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = DateTimeValidator.class)
public @interface DateTime {

String message() default "格式错误";

String format() default "yyyy-MM-dd";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
然后看验证类 实现ConstraintValidator<A extends Annotation, T>即可 A是注解 T是标注的参数

public class DateTimeValidator implements ConstraintValidator<DateTime, String> {

private DateTime dateTime;

@Override
public void initialize(DateTime dateTime) {
this.dateTime = dateTime;
}

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离
if (value == null) {
return true;
}
String format = dateTime.format();
if (value.length() != format.length()) {
return false;
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
try {
simpleDateFormat.parse(value);
} catch (ParseException e) {
return false;
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
然后这样用就行啦

@Validated
@RestController
public class ValidateController {
@GetMapping("/test")
public String test(@DateTime(message = "您输入的格式错误,正确的格式为:{format}", format = "yyyy-MM-dd HH:mm") String date) {
return "success";
}
}
1
2
3
4
5
6
7
8
JSR-303 注释介绍
hibernate-validator均实现了 JSR-303 这里只列举了 javax.validation 包下的注解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 验证包,里面包含了一些 javax.validation 没有的注解,有兴趣的可以看看

注解 说明
@NotNull 限制必须不为null
@NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在 min 到 max 之间(也可以用在集合上)
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@Null 限制只能为null(很少用)
@AssertFalse 限制必须为false (很少用)
@AssertTrue 限制必须为true (很少用)
@Past 限制必须是一个过去的日期
@Future 限制必须是一个将来的日期
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用)
哦对,这些校验不仅能在controller层用 在任何地方都可以的

一起来学SpringBoot(十七)优雅的参数校验的更多相关文章

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

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

  2. SpringBoot Validation优雅的全局参数校验

    前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...

  3. 测试开发专题:如何在spring-boot中进行参数校验

    上文我们讨论了spring-boot如何去获取前端传递过来的参数,那传递过来总不能直接使用,需要对这些参数进行校验,符合程序的要求才会进行下一步的处理,所以本篇文章我们主要讨论spring-boot中 ...

  4. springboot 接口参数校验

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

  5. 【快学SpringBoot】Spring Cache+Redis实现高可用缓存解决方案

    前言 之前已经写过一篇文章介绍SpringBoot整合Spring Cache,SpringBoot默认使用的是ConcurrentMapCacheManager,在实际项目中,我们需要一个高可用的. ...

  6. 【快学springboot】12.实现拦截器

    前言 之前在[快学springboot]6.WebMvcConfigurer配置静态资源和解决跨域里有用到WebMvcConfigurer接口来实现静态资源的映射和解决跨域请求,并且在文末还说了Web ...

  7. SpringBoot Validation参数校验 详解自定义注解规则和分组校验

    前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...

  8. 【springboot】@Valid参数校验

    转自: https://blog.csdn.net/cp026la/article/details/86495659 扯淡: 刚开始写代码的时候对参数的校验要么不做.要么写很多类似 if( xx == ...

  9. 全局异常处理及参数校验-SpringBoot 2.7 实战基础 (建议收藏)

    优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验 前后端分离开发非常普遍,后端处理业务,为前端提供接口.服务中总会出现很多运行时异常和业务异常,本文主要讲解在 Sp ...

随机推荐

  1. CentOS 7下Keepalived + HAProxy 搭建配置详解

    第一步:准备 1. 简介 本文搭建的是利用 Keepalived 实现 HAProxy 的热备方案,即两台主机上的 HAProxy 实例同时运行,其中全总较高的实例为 MASTER,MASTER出现异 ...

  2. 【POJ 1947】 Rebuilding Roads

    [题目链接] 点击打开链接 [算法] f[i][j]表示以i为根的子树中,最少删多少条边可以组成j个节点的子树 树上背包,即可 [代码] #include <algorithm> #inc ...

  3. Python3中 对local和nonlocal 关键字的改善认识(新手向)

    nonlocal关键字用来在函数或其他作用域中使用外层(非全局)变量. nonlocal用于声明,修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量,如下实例: #!/usr/bi ...

  4. gdb core调试

    原文链接 http://blog.163.com/lanka83/blog/static/32637615200801793020182/http://blog.csdn.net/taina2008/ ...

  5. 洛谷 P1315 观光公交 —— 贪心

    题目:https://www.luogu.org/problemnew/show/P1315 问题是想不明白改动一条边会对后面造成怎样的影响: 实际上影响的会是一段,当某个车站出发时间受其来人牵制时, ...

  6. bzoj3668 [Noi2014]起床困难综合症——贪心

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=3668 一开始想着倒序推回去看看这一位能不能达到来着,因为这样好中途退出(以为不这样会T): ...

  7. Python基础第九天

    一.内容

  8. bzoj 3944: Sum【莫比乌斯函数+欧拉函数+杜教筛】

    一道杜教筛的板子题. 两个都是积性函数,所以做法是一样的.以mu为例,设\( f(n)=\sum_{d|n}\mu(d) g(n)=\sum_{i=1}^{n}f(i) s(n)=\sum_{i=1} ...

  9. [App Store Connect帮助]七、在 App Store 上发行(3.4)提交至“App 审核”:将构建版本从审核中移除

    若要停止“App 审核”流程,您可以将该 App 版本从 App 审核中移除.要执行此项操作,App 状态必须为下列之一: 正在等待出口合规检查 正在等待审核 正在审核 等待开发者发布 等待 Appl ...

  10. 【HDU - 1241】Oil Deposits(dfs+染色)

    Oil Deposits Descriptions: The GeoSurvComp geologic survey company is responsible for detecting unde ...