本节目标
- springboot校验框架validator简介
- springboot集成Validator校验框架
- 全局异常捕获校验框架异常
- 分组校验
- 自定义校验注解
validator简介
在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,例如登录的时候需要校验用户名密码是否为空,创建用户的时候需要校验邮件、手机号码格式是否准确。Validator框架就是为了解决开发人员在开发的时候少写代码,提升开发效率;Validator专门用来进行接口参数校验,例如常见的必填校验,email格式校验等。Spring Boot中使用Hibernate Validator进行后端效验,而Hibernate Validator遵循了JSR303参数校验规范并且进行了扩展。
JSR(Java Specification Requests)是Java界的重要标准;JSR又细分很多标准,其中JSR303就代表Bean Validation。更多细节可参考:jcp.org/en/jsr/deta…
简言之,validator是用来进行参数校验的一个框架。
validator支持的注解
| 注解 | 功能 |
|---|---|
| @AssertFalse | 可以为null,如果不为null的话必须为false |
| @AssertTrue | 可以为null,如果不为null的话必须为true |
| @DecimalMax | 设置不能超过最大值 |
| @DecimalMin | 设置不能超过最小值 |
| @Digits | 设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
| @Future | 日期必须在当前日期的未来 |
| @Past | 日期必须在当前日期的过去 |
| @Max | 最大不得超过此最大值 |
| @Min | 最大不得小于此最小值 |
| @NotNull | 不能为null,可以是空 |
| @Null | 必须为null |
| @Pattern | 必须满足指定的正则表达式 |
| @Size | 集合、数组、map等的size()值必须在指定范围内 |
| 必须是email格式 | |
| @Length | 长度必须在指定范围内 |
| @NotBlank | 字符串不能为null,字符串trim()后也不能等于“” |
| @NotEmpty | 不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于“” |
| @Range | 值必须在指定范围内 |
| @URL | 必须是一个URL |
集成Validator
1. SpringBoot三板斧之添加依赖
<!-- springboot -->
<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>
注:从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。
2.定义请求的Bean
@Data
public class UserBO {
@Length(min = 6,max = 12,message = "userid长度必须位于6到12之间")
private String userid;
@Range(min = 1, max = 100, message = "年龄填写范围[1-100]")
private Integer age;
@NotBlank(message = "名字为必填项")
private String name;
@Email(message = "请填写正确的邮箱地址")
private String email;
@Pattern(regexp = "男|女|未知", message = "性别请填写男、女或者未知")
private String sex;
@NotEmpty(message = "级别不能为空")
private String level;
}
3.测试controller
@Validated
@RestController
public class ValidatorTestController {
/**
* RequestBody校验
*/
@PostMapping("/test1")
public R<UserBO> test1(@Validated @RequestBody UserBO userBO){
return R.ok(userBO);
}
/**
* Form表单校验
*/
@PostMapping(value = "/test2")
public R<UserBO> test2(@Validated UserBO userBO){
return R.ok(userBO);
}
/**
* 单个参数校验.当使用单参数校验时需要在Controller上加上@Validated注解,否则不生效
*/
@GetMapping(value = "/test3")
public R<Void> test3(@Email(message = "请填写正确的邮箱地址") String email){
return R.ok(StrUtil.format("{}校验成功!", email));
}
}
4.全局异常捕获
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 默认全局异常处理。
* @param e the e
* @return ResultData
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public R<Void> exception(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
return R.fail(e.getMessage());
}
/**
* 捕获校验框架Validator抛出的异常
* BindException: 表单数据校验失败
* ConstraintViolationException: 单参数校验失败
* MethodArgumentNotValidException: RequestBody校验失败
*/
@ExceptionHandler(value = {BindException.class, ConstraintViolationException.class, MethodArgumentNotValidException.class})
public R<Void> handleValidatedException(Exception e) {
if (e instanceof MethodArgumentNotValidException) {
// RequestBody校验失败
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
return R.fail(ex.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining("; "))
);
} else if (e instanceof ConstraintViolationException) {
// 单参数校验失败
ConstraintViolationException ex = (ConstraintViolationException) e;
return R.fail(ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "))
);
} else if (e instanceof BindException) {
// 表单数据校验失败
BindException ex = (BindException) e;
return R.fail(ex.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining("; "))
);
}
return R.fail();
}
}
5.postman测试一下
比较@Valid 和 @Validated 两个注解
在某些项目中两种注解都有可能看到,@Valid注解与@Validated注解功能大部分类似;两者的不同主要在于:
- @Valid属于javax下的,而@Validated属于spring框架下的
- @Valid支持嵌套校验、而@Validated不支持
- @Validated支持分组,而@Valid不支持
- @Validated 进行校验的时候,当校验不通过的时候,程序会抛出400异常,阻止方法中的代码执行,这时需要 再写一个全局校验异常捕获处理类,然后返回校验提示。
总结:
在springboot框架下,尽量都用@Validated即可。
分组校验
一个请求对象在添加时需要的填写的数据,在更改时未必需要;反之亦然。就如UserBO对象,userid这个值,在添加时不影响提供,但是在编辑时则必须提供。这时就需要用到分组校验了。
1.定义分组接口
public interface ValidGroup extends Default {
interface Create extends ValidGroup{
}
interface Update extends ValidGroup{
}
interface Query extends ValidGroup{
}
interface Delete extends ValidGroup{
}
}
2.请求对象指定分组
@Data
public class UserBO {
@Length(min = 6,max = 12,message = "userid长度必须位于6到12之间")
@NotBlank(groups = {ValidGroup.Update.class}, message = "用户id不能为空")
private String userid;
@Range(min = 1, max = 100, message = "年龄填写范围[1-100]")
@NotNull(groups = {ValidGroup.Create.class, ValidGroup.Update.class},message = "年龄为必填项")
private Integer age;
@NotBlank(groups = {ValidGroup.Create.class, ValidGroup.Update.class},message = "名字为必填项")
private String name;
@Email(message = "请填写正确的邮箱地址")
private String email;
@Pattern(regexp = "男|女|未知", message = "性别请填写男、女或者未知")
@NotBlank(groups = {ValidGroup.Create.class, ValidGroup.Update.class},message = "性别为必填项")
private String sex;
@NotEmpty(message = "级别不能为空")
private String level;
}
注意:
如果对所有分组都要求不为空的话,其实是不用指定分组的。比如上面的age,name,sex。
3.controller中指定分组
@PostMapping(value = "/add")
public R<Void> add(@Validated(value=ValidGroup.Create.class) @RequestBody UserBO userBO){
return R.ok();
}
@PostMapping(value = "/update")
public R<Void> update(@Validated(value=ValidGroup.Update.class) @RequestBody UserBO userBO){
return R.ok();
}
4.分组测试
可以看到相同的请求参数,在更新请求中,是报错的。
自定义校验注解
虽然Validator在大多数业务场景都够用了,尤其是支持正则表达式这个注解,能解决很多复杂的情况。但是我们依然有可能遇到一些需要自定义注解的场景。比如:我们在请求信息中一般会有一个sign验证的验证码。这个验证码的规则可能就比较复杂,这时我们就需要自定义注解进行验签了。
1.验签码校验需求描述
签名计算规则如下:
- 首先将请求参数中的每一个字段按照0-9-A-Z-a-z的顺序排序(ASCII字典序),若遇到相同首字母,则看第二个字母,以此类推。
- 排序后的参数以key=value形式使用“&”字符连接,并拼接上通讯密钥key值(注意:通讯密钥key直接拼接),即为待签名字符串。
- 使用SHA256签名算法计算待签名字符串的SHA256值,然后转换成16进制字符串。
注意:
根据HTTP协议要求,传递参数的值中如果存在特殊字符(如:&、@等),则该值需要做URLEncoding UTF-8编码处理,这样接收方才能正确接收到参数信息。这种情况下,待签名数据应该是原始值而不是Encoding编码之后的值。没有值的参数,包含空字符串、空格、全空白字符、若干制表符等,如:""," "," ","\t","\n","\r"以及空字符串、空格、全空白字符、若干制表符的组合,接口无需传入,亦无需参与计算签名
2.自定义注解
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SignValidator.class})
public @interface SignCheck {
//签名字段名,默认是sign,可以认为指定
String signFieldName() default "sign";
//默认提示的信息
String message() default "验签失败";
//分组的指定
Class<?>[] groups() default { };
//有效负载。用于保存一些关键信息。
Class<? extends Payload>[] payload() default { };
}
-
@Documented
表明这个注解应该被 javadoc此类的工具文档化
-
@Target
表明这个注解应该在哪里使用
- TYPE :用在类、接口、或者枚举上
- FIELD:用在字段(包括枚举常量)上
- METHOD:用在方法上
- PARAMETER:用在参数上
- CONSTRUCTOR:用在构造方法上
- LOCAL_VARIABLE:用在局部变量上
- ANNOTATION_TYPE:用在注解上
- PACKAGE:用在包上
-
@Retention
注解的生命周期。
- SOURCE : 保留在源码级别,编译成class后就丢失
- CLASS:保留在class文件中,在JVM运行时丢失。这是默认值
- RUNTIME:在JVM运行时保留,可以被反射调用到
-
@Constraint
用来指定由哪个类来具体执行校验逻辑。这里指名的target为注解,说明自定义的注解是可以加这个注解的
@Documented @Target({ ANNOTATION_TYPE }) @Retention(RUNTIME) public @interface Constraint { ...... }
3.自定义校验逻辑实现
@Slf4j
public class SignValidator implements ConstraintValidator<SignCheck, Object> {
private SignCheck signCheck;
@Override
public void initialize(SignCheck constraintAnnotation) {
this.signCheck = constraintAnnotation;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if(value instanceof CommonRequest) {
CommonRequest request = (CommonRequest) value;
//判断请求对象中的sign是否有值.如果没有sign字段,或者sign的字段值为空,直接返回校验失败
Object fieldValue = BeanUtil.getFieldValue(request, this.signCheck.signFieldName());
if(ObjectUtil.isNull(fieldValue) || StrUtil.isBlank(String.valueOf(fieldValue))){
return false;
}
//按照预先定义的逻辑,对请求对象生成签名
String generateSign = generateSign(request);
return StrUtil.equalsIgnoreCase(request.getSign(), generateSign);
}
return false;
}
//按照预先定义的逻辑,对请求对象生成签名
public String generateSign(CommonRequest request) {
// 获得对象的所有属性进行排序
Map<String, Object> beanMap = BeanUtil.beanToMap(request);
// 对所有属性进行排序
TreeMap<String, Object> treeMap = new TreeMap<>(beanMap);
beanMap.keySet().forEach(key -> treeMap.put(key, beanMap.get(key)));
//用=号和&号连接参数.排除掉sign字段
StringBuilder sb = new StringBuilder();
for (String key : treeMap.keySet()) {
if(this.signCheck.signFieldName().equals(key)){
continue;
}
sb.append(key).append("=").append(treeMap.get(key)).append("&");
}
if(sb.length() > 0){
sb.deleteCharAt(sb.length() - 1);
//拼接私钥
sb.append("1234567890");
}
//进行sha256加密
return DigestUtil.sha256Hex(sb.toString());
}
}
4.加上注解
@Data
@SignCheck
public class CommonRequest {
/**
* 版本号。固定版本号为V1.0
*/
@NotEmpty(message = "版本号不能为空")
private String version;
/**
* 签名。采用的算法:SHA256(除sign字段的其他字段ASCII字典升序)
*/
@NotEmpty(message = "签名不能为空")
private String sign;
}
5.测试
代码地址
gitee.com/mayuanfei/S…下的springboot11
记忆印记
- Validator框架是一个参数校验框架。用于验证请求参数的
- 采用全局异常处理方式来统一返回校验框架抛出来的异常
- 在springboot框架中统一使用@Validated注解来进行参数验证
- 知道分组校验的应用场景
- 知道有自定义校验注解,回头需要的时候来这里查查