概述
日常开发中,对入参进行参数校验是必不可少的一个环节。 而使用最多的就是Validator框架 。
Validator校验框架遵循了JSR-303 【Java Specification Requests】验证规范 。
这里实践下,在boot项目中如何优雅的集成参数校验框架
Validator常用校验规则
使用Validator
添加依赖
boot 2.3 以后版本的pom信息如下
1 2 3 4 5 6 7 8 9 10 11
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
|
springboot 2.3版本之前只需要引入 spring-boot-starter-web 即可 ,已经包含了
添加携带有参数校验的实体类
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 29 30 31 32
| package com.artisan.vo;
import lombok.Data; import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty;
@Data public class Artisan {
private String id;
@NotBlank(message = "名字为必填项") private String name;
@Length(min = 8, max = 12, message = "password长度必须位于8到12之间") private String password;
@Email(message = "请填写正确的邮箱地址") private String email;
private String sex;
@NotEmpty(message = "Code不能为空") private String code; }
|
添加控制器用于接收请求并验证
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| package com.artisan.controller;
import com.artisan.vo.Artisan; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Email;
@RestController @Slf4j @Validated @RequestMapping("/valid") public class ArtisanController {
@PostMapping("/testJson") public String testJson(@Validated @RequestBody Artisan artisan) { log.info("InComing Param {}", artisan); return "testJson valid success"; }
@PostMapping(value = "/testForm") public String testForm(@Validated Artisan artisan) { log.info("InComing Param is {}", artisan); return "testForm valid success"; }
@PostMapping(value = "/testParma") public String testParma(@Email String email) { log.info("InComing Param is {}", email); return "testParma valid success"; } }
|
发起请求
/testJson接口
测试第一个接口【/testJson】,该接口接收一个JSON数据
请求参数邮箱参数不合格
结果:
可以看到抛出的异常为: org.springframework.web.bind.MethodArgumentNotValidException
/testFrom
测试表单数据的接口
结果:
可以看到抛出的异常为: org.springframework.validation.BindException
/testParams
测试参数是在url的数据
结果:
可以看到抛出的异常为:javax.validation.ConstraintViolationException
存在的问题
且不说好不好看, 不管怎么样,现在是通过Validation框架实现了校验。 当然了,我们的追求肯定不是这样的,Validator校验框架返回的错误提示太臃肿了 ,格式啥的都不一样,很难搞哦, 怎么给前台返回????
使用 统一格式 + 全局异常Handler 优化
增加统一返回 和 [[Spring boot的全局异常处理器|全局异常Handler]],单独拦截参数校验的三个异常:
javax.validation.ConstraintViolationException
org.springframework.validation.BindException
org.springframework.web.bind.MethodArgumentNotValidException
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 29 30 31 32 33 34 35 36 37 38 39 40
|
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class}) public ResponseEntity<ResponseData<String>> handleValidatedException(Exception e) { ResponseData<String> resp = null;
if (e instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e; resp = ResponseData.fail(HttpStatus.BAD_REQUEST.value(), ex.getBindingResult().getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof ConstraintViolationException) { ConstraintViolationException ex = (ConstraintViolationException) e; resp = ResponseData.fail(HttpStatus.BAD_REQUEST.value(), ex.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining("; ")) ); } else if (e instanceof BindException) { BindException ex = (BindException) e; resp = ResponseData.fail(HttpStatus.BAD_REQUEST.value(), ex.getAllErrors().stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.joining("; ")) ); }
log.error("参数校验异常:{}", resp.getMessage()); return new ResponseEntity<>(resp, HttpStatus.BAD_REQUEST); }
|
使用统一异常处理之后重新测试
参数分组
我们经常会碰到这样的一个场景: 新增的时候某些字段为必填(比如密码), 更新的时候非必填。
这样该如何做呢;
首先得明确,如果给校验分场景来校验的话,就会出现一个对象的字段上会有若干个注解来校验,同时需要指定这个校验是在指定场景来生效;
后面在代码里执行校验的时候,也需要指定当下场景启用校验的场景是什么。
以上的场景就可以理解为分组的概念。
执行步骤
定义分组接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import javax.validation.groups.Default;
public interface CustomValidateGroup extends Default {
interface Crud extends CustomValidateGroup { interface Create extends Crud {
}
interface Update extends Crud {
}
interface Query extends Crud {
}
interface Delete extends Crud {
} } }
|
定义一个分组接口CustomValidateGroup
让其继承javax.validation.groups.Defaul
t,再在分组接口中定义出多个不同的操作类型,Create,Update,Query,Delete.
接口里面不需要写内容,这里只是对校验场景进行一个区分。
给参数分配分组
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
|
@Data public class User { private String id; @NotNull(message = "名字为必填项") private String name; @Length(min = 8, max = 12, message = "password长度必须位于8到12之间",groups = CustomValidateGroup.Crud.Create.class) @NotNull(groups = CustomValidateGroup.Crud.Create.class,message = "密码不能为空") @Null(groups = CustomValidateGroup.Crud.Update.class) private String password; @Email(message = "请填写正确的邮箱地址") private String email; private String sex; @NotNull(message = "Code不能为空") private String code; }
|
以上代表在对 password
字段校验的时候有2个场景,一个在 Create
场景下需要校验长度和不能为空,而在 Update
场景下该参数可以为空。
指定分组
给需要参数校验的方法指定分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@PostMapping(value = "/addUser") public String add(@Validated(value = CustomValidateGroup.Crud.Create.class) @RequestBody User user){ log.info("InComing Param is {}", user); return "add valid success"; }
@PostMapping(value = "/updateUser") public String update(@Validated(value = CustomValidateGroup.Crud.Update.class) @RequestBody User user){ log.info("InComing Param is {}", user); return "update valid success"; }
|
在创建用户下,不填密码:
而在更新用户时,不传密码:
对于未指定分组的则使用的是默认分组 。 比如由于email属于默认分组,而我们的分组接口CustomValidateGroup已经继承了Default分组,所以也是可以对email字段作参数校验的;
如果CustomValidateGroup没有继承Default分组,那在代码属性上就需要加上@Validated(value = {ValidGroup.Crud.Create.class, Default.class}才能让email字段的校验生效。
自定义注解
Validation允许用户自定义校验,Validation 提供的注解基本上够用,但是复杂的校验,我们还是需要自己定义注解来实现自动校验。
自定义注解分为2步,一个是创建自己自己的注解,第二个是创建校验逻辑类;
创建注解
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 29 30 31 32 33
|
@Target({TYPE,FIELD,METHOD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = IDCardValidator.class) public @interface CheckPhone {
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String message() default "手机号格式错误";
String phone() default ""; }
|
在自定义注解的时候,groups
、payload
、message
这3个字段是必需添加的,其他的根据业务去添加;
添加在注解上的注解,还需要添加一个注解 Constraint
,代表着该注解由哪个校验类负责进行校验。
创建校验类
校验类需要实现 ConstraintValidator
接口,该接口接收2个泛型参数, 第一个参数是 自定义注解类型,第二个参数是 被注解字段的类。
校验的类型如果是基本类型那就直接填入类型,如果是多个字段需要校验,那就传入自定义类型;这里直接传入用户对象
该接口中需要实现2个方法,initialize()
和isValid()
。顾名思义就是用来初始化注解和进行校验的方法;
initialize()
方法用于初始化注解,拿到在使用注解时传入的值,比如校验错误时所提示的信息,就可以在使用注解的时候进行重写。
isValid()
方法就用于进行校验,该方法会传入一个值,该值就是被注解所标注的字段的值。
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 29 30 31 32 33 34 35 36
|
@Slf4j public class IDCardValidator implements ConstraintValidator<CheckIDCard,String> { private static final Pattern idCard_pattern = Pattern.compile("^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$"); private String IDCard;
@Override public void initialize(CheckIDCard constraintAnnotation) {
log.info("初始化注解......"); IDCard = constraintAnnotation.IDCard(); }
@Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { Matcher matcher = idCard_pattern.matcher(s); System.out.println(); return matcher.matches(); } }
|
使用注解
在用户类添加身份证字段
1 2
| @CheckIDCard(message = "身份证格式错误!") private String idCard;
|
发起请求