一起来学SpringBoot(十七)优雅的参数校验
参数校验
在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:
验证代码繁琐,重复劳动
方法内代码显得冗长
每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码
你看这样?我感觉不行 ~有啥好办法不
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(十七)优雅的参数校验的更多相关文章
- 【快学springboot】4.接口参数校验
前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...
- SpringBoot Validation优雅的全局参数校验
前言 我们都知道在平时写controller时候,都需要对请求参数进行后端校验,一般我们可能会这样写 public String add(UserVO userVO) { if(userVO.getA ...
- 测试开发专题:如何在spring-boot中进行参数校验
上文我们讨论了spring-boot如何去获取前端传递过来的参数,那传递过来总不能直接使用,需要对这些参数进行校验,符合程序的要求才会进行下一步的处理,所以本篇文章我们主要讨论spring-boot中 ...
- springboot 接口参数校验
前言 在开发接口的时候,参数校验是必不可少的.参数的类型,长度等规则,在开发初期都应该由产品经理或者技术负责人等来约定.如果不对入参做校验,很有可能会因为一些不合法的参数而导致系统出现异常. 上一篇文 ...
- 【快学SpringBoot】Spring Cache+Redis实现高可用缓存解决方案
前言 之前已经写过一篇文章介绍SpringBoot整合Spring Cache,SpringBoot默认使用的是ConcurrentMapCacheManager,在实际项目中,我们需要一个高可用的. ...
- 【快学springboot】12.实现拦截器
前言 之前在[快学springboot]6.WebMvcConfigurer配置静态资源和解决跨域里有用到WebMvcConfigurer接口来实现静态资源的映射和解决跨域请求,并且在文末还说了Web ...
- SpringBoot Validation参数校验 详解自定义注解规则和分组校验
前言 Hibernate Validator 是 Bean Validation 的参考实现 .Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的 ...
- 【springboot】@Valid参数校验
转自: https://blog.csdn.net/cp026la/article/details/86495659 扯淡: 刚开始写代码的时候对参数的校验要么不做.要么写很多类似 if( xx == ...
- 全局异常处理及参数校验-SpringBoot 2.7 实战基础 (建议收藏)
优雅哥 SpringBoot 2.7 实战基础 - 08 - 全局异常处理及参数校验 前后端分离开发非常普遍,后端处理业务,为前端提供接口.服务中总会出现很多运行时异常和业务异常,本文主要讲解在 Sp ...
随机推荐
- Enterprise Architect 生成项目类图
Enterprise Architect使用教程: https://blog.csdn.net/chenglc1612/article/details/81083151 主要流程 --到此-自动生成完 ...
- arm位清零bic指令
(1)指令的语法格式 BIC{<cond>}{S} <Rd>,<Rn>,<shifter_operand> BIC(Bit Clear)位清零指令,将寄 ...
- HDU3488 Tour —— 二分图最大权匹配 KM算法
题目链接:https://vjudge.net/problem/HDU-3488 Tour Time Limit: 3000/1000 MS (Java/Others) Memory Limit ...
- 织梦DEDE系统跨站跨数据库调用数据显示
调用方法 本标签的调用格式为: {dede:sql sql="一条完整的SQL语句" appname="数据库配置参数"}您的底层模板{/dede:sql} 稍 ...
- Linux 杀死所有进程
方法一: sudo killall -9 netease-cloud-music 这种方法,必须要写全称. sudo netease-cloud-music QStandardPaths: XDG_R ...
- html5--switch选择结构的优化
html5--switch选择结构的优化 问题: 使用循环语句判断月份是31天还是30天 两点提示: 使用switch多条件判断语句 合理的省略break优化代码 <!DOCTYPE html& ...
- 1.import和include区别 2.NSLog 和printf区别 3.创建对象做的事情 4. 类和对象方法比较 5 匿名对象优缺点 6. 封装 7.作用域范围 8.id和instancetype 9.自定义构造方法规范 10.nil和Nil及NULL、NSNull区别
1.import和include的区别: import可以防止头文件的重复包含 2.NSLog 和printf的区别: 1,NSLog可以自动换行, 输出调试信息, printf不能. 2,NSLog ...
- 转:IIS MVC 发布错误 403.14-Forbidden Web 服务器被配置为不列出此目录的内容
访问网址:http://blog.csdn.net/csethcrm/article/details/37820135 有两个地方需要配置: 1.web.config中的节点: <system. ...
- bzoj 4037: [HAOI2015]数字串拆分【dp+矩阵加速】
首先f长得就很像能矩阵优化的,先构造转移矩阵(这里有一点神奇的地方,我看网上的blog和我构造的矩阵完全不一样还以为我的构造能力又丧失了,后来惊奇的发现我把那篇blog里的构造矩阵部分换成我的构造方式 ...
- mysql case 列名 when 和 case when的区别
最近写了一个sql,才发现有些情况不能用case 列名 when ( and then and and 7.9 then '中' else '差' END ) score_type, 我发现这样写查出 ...