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

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

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. XMU 1056 瞌睡 vs 听课 【动态规划】

    1056: 瞌睡 vs 听课 Time Limit: 500 MS  Memory Limit: 64 MBSubmit: 19  Solved: 6[Submit][Status][Web Boar ...

  2. 注册中心Eureka页面添加用户认证

    我们需要登录即可访问到Eureka服务,这样其实是不安全的 为Eureka添加用户认证. 第一步,为itcast-microservice-eureka添加安全认证依赖: 第二步,增加applicat ...

  3. jquery a

    <!DOCTYPE html><html><head><script src="//ajax.googleapis.com/ajax/libs/jq ...

  4. java抛出异常后,后续代码是否可继续执行

    参考:https://www.cnblogs.com/wangyingli/p/5912269.html 仅此可正常执行异常后内容 try{ throw new Exception("参数越 ...

  5. 洛谷 P3732 [HAOI2017]供给侧改革【trie树】

    参考:http://blog.csdn.net/di4covery/article/details/73065684 我以为是后缀数组+某某数据结构,结果居然是01trie!!题解说"因为是 ...

  6. 洛谷P2607 [ZJOI2008]骑士(基环树)

    传送门 首先这是一个有$n$个点$n$条边的图(据大佬们说这玩意儿叫做基环树?) 不难(完全没有)发现每个连通块里最多只有一个环 那么找到这个环,然后把它断开,再对它的两个端点分别跑树形dp 设$dp ...

  7. [App Store Connect帮助]八、维护您的 App(4.2)查看评分与评论

    您可以查看 App 的总评分或单个顾客评论.如有必要,您可以针对某条评论报告问题. [注]顾客可以为您的 iOS 和 macOS App 评分并撰写评论,但只能为 Apple TVOS App 评分. ...

  8. SpringMVC异步调用,Callable和DeferredResult的使用

    Callable和DeferredResult都是springMVC里面的异步调用,Callable主要用来处理一些简单的逻辑,DeferredResult主要用于处理一些复杂逻辑 1.Callabl ...

  9. 7步教你使用git命令上传本地代码至github仓库(小白向)

    前言 1:首先你需要知道github和git分别是什么?(众所周知github是全球最大同性交友网站233333) github:国外一个免费托管代码的开源网站,每位注册用户都拥有专属的个人仓库(个人 ...

  10. python3 写CSV文件多一个空行的解决办法

    Python文档中有提到: open('eggs.csv', newline='') 也就是说,打开文件的时候多指定一个参数.Python文档中也有这样的示例: import csvwith open ...