如何优雅实现后端接口统一参数的校验

201 阅读5分钟

一、业务场景****

在日常的开发中,参数校验是非常重要的一个环节,严格参数校验会减少很多出bug的概率,增加接口的安全性。 与第三方平台对接,第三方调用接口实现数据上报,由于接口传参较多,要对每一个参数写代码做校验会非常麻烦,如果使用第三方框架自带的校验功能实现对参数的统一校验,大大减少代码量,通过注解的方式,使代码更加简洁。

二、5种实现方案****

2.1 集成hibernate-validator框架(推荐)****

集成hibernate-validator框架,基于框架提供的注解实现数据验证(应用比较广泛,与Spring Boot集成比较好,推荐使用)

 

2.2 pom.xml引入依赖****

*
*
org.hibernate.validator
hibernate-validator
6.2.0.Final

2.3 方案1:使用springboot validator框架自带的注解实现数据验证****

2.3.1 在参数实体中加类似注解,类似@NotBlank,@NotEmpty,@NotNull,@Range,示例代码如下:

@Data
public class SysUserDTO implements Serializable {
private Integer id;
@NotBlank(message = "用户名不能为空")
private String username;
private String password;
}

2.3.2 常见坑点:@NotNull 和 @NotEmpty 和@NotBlank 有什么区别?

@NotEmpty 用在集合类上

@NotBlank 用在String上

@NotNull 用在基本类型上

 

常用注解如下:

 

@Null    元素必须为null

@NotNull    元素不能null

@AssertTrue    元素必须为true

@AssertFalse    元素必须是false

@Min(value)    元素必须是一个数字,其值必须大于等于指定的最小值

@Max(value)    元素必须是一个数字,其值必须小于等于指定的最大值

@DecimalMin(value)    元素必须是一个数字,其值必须大于等于指定的最小值

@DecimalMax(value)    元素必须是一个数字,其值必须小于等于指定的最大值

@Size(max,min)    元素的大小必须在指定的范围内

@Digits(integer,fraction)    元素必须是一个数字,其值必须在可接受的范围内

@Past    元素必须是一个过去的日期

@Future    元素必须是一个将来的日期

@Pattern(value)    元素必须符合指定的正则表达式

@Email    元素必须是电子邮箱地址

@Length    字符串的大小必须在指定的范围内

@NotEmpty    字符串必须非空

@Range    元素必须在合理的范围内

2.3.3 在方法的参数那里加上@Validated,示例代码如下:

@RestController
@RequestMapping("/sys_user")
public class SysUserController {

    @PostMapping("/add")
public RespResult addSysUser(@Validated @RequestBody SysUserDTO sysUserDTO){
return RespResult.success(sysUserDTO);
}

}

2.3.4 postman测试,结果如下:

 

 

2.4 方案2:开发一个验证的工具类ValidatorUtil.java,提供校验参数的方法****

开发一个验证的工具类ValidatorUtil.java, 提供校验参数的方法,抛出ParamException运行时异常(推荐,比较优雅)

2.4.1 在参数实体属性中加类似校验注解,类似@NotEmpty,@NotNull,@Min @Max @Range,@Pattern如下:

@Data
public class SysUserDTO implements Serializable {
private Integer id;
@NotBlank(message = "用户名不能为空")
@Length(min = 3,max = 16,message = "用户名长度在3~16个字符")
private String username;
private String password;
}

2.4.2 自定义ValidatorUtil类,在业务的方法中,调用ValidatorUtil.validate(参数)

package com.bmbfm.common.validator;
import com.bmbfm.common.base.RespResultCode;
import com.bmbfm.common.exception.ParamException;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/***
*** hibernate-validator校验工具类**
*/
*public class ValidatorUtil {
private static Validator validator;

    static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}

    public static void validate(Object object, Class<?>... groups) throws ParamException {
Set<ConstraintViolation> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
StringBuilder msg = new StringBuilder();
for (ConstraintViolation constraint : constraintViolations) {
msg.append(constraint.getMessage()).append(";");
}
throw new ParamException(RespResultCode.ERR_PARAM_NOT_LEGAL.getCode(), msg.toString());
}
}
}

2.4.3 定义封装好的业务异常ParamException,继承运行时异常

package com.bmbfm.common.exception;

import lombok.NoArgsConstructor;

/***
*** 自定义校验参数异常类**
*/
*@NoArgsConstructor
public class ParamException extends RuntimeException {
private static final long serialVersionUID = 1L;

    public ParamException(String message) {
super(message);
}

    public ParamException(int code, String message) {
super(message);
}

    public ParamException(Throwable cause) {
super(cause);
}

    public ParamException(String message, Throwable cause) {
super(message, cause);
}

    public ParamException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

2.4.4 postman测试,结果如下:

 

 

2.5 方案3:定义通过全局异常拦截器GlobalExceptionHandler拦截异常****

2.5.1 定义通过全局异常拦截器GlobalExceptionHandler拦截ParamException异常,返回参数错误的异常code和message,代码如下:

package com.bmbfm.common.exception;

import com.bmbfm.common.base.RespResult;
import com.bmbfm.common.base.RespResultCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/***
*** 自定义全局异常处理类**
*/
*@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ParamException.class)
public RespResult handleError(ParamException e) {
log.error("参数不合法 :{}", e.getMessage(), e);
return RespResult.error(RespResultCode.ERR_PARAM_NOT_LEGAL.getCode(),
RespResultCode.ERR_PARAM_NOT_LEGAL.getMessage(), e.getMessage());
}

    @ExceptionHandler(MethodArgumentNotValidException.class)
public RespResult handleError(MethodArgumentNotValidException e) {
log.error("Method Request Not Valid :{}", e.getMessage(), e);
BindingResult bindingResult = e.getBindingResult();
FieldError fieldError = bindingResult.getFieldError();
String message = String.format("%s:%s", fieldError.getField(), fieldError.getDefaultMessage());
return RespResult.error(RespResultCode.ERR_PARAM_NOT_LEGAL.getCode(), RespResultCode.ERR_PARAM_NOT_LEGAL.getMessage(), message);
}

}

 

2.5.2 postman测试,结果如下:

 

 

2.6 方案4:自定义hibernate-validator校验器****

一般情况,内置的校验器可以解决很多问题。但也有无法满足情况的时候,此时,我们可以实现validator的接口,自定义自己需要的验证器。

2.6.1 与普通注解相比,这种自定义注解需要增加元注解@Constraint,并通过validatedBy参数指定验证器。

2.6.2 依据JSR规范,定义三个通用参数:message(校验失败保存信息)、groups(分组)和payload(负载)

2.6.3 定义一个参数验证器,实现参数校验的逻辑

2.6.4 自定义一个IdCard身份证校验的注解、IdCard自定义校验器,代码如下:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy= IdCardValidator.class)
public @interface IdCard {

    String message() default "身份证号码格式不对";

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

    Class<? extends Payload>[] payload() default {};
}

 

 

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class IdCardValidator implements ConstraintValidator<IdCard, String> {
/***
*** 身份证规则校验正则表达式**
*/
***private String reg = "^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$";
private Pattern pattern = Pattern.compile(reg);

    public IdCardValidator() {
}

    public boolean isValid(String value, ConstraintValidatorContext arg1) {
if (value == null) {
return true;
}
Matcher m = pattern.matcher(value);
return m.find();
}
}

2.6.5 在参数实体中加类似注解,类似@NotBlank,@IdCard,示例代码如下:

@Data
public class SysUserDTO implements Serializable {
private Integer id;
@NotBlank(message = "用户名不能为空")
@Length(min = 3,max = 16,message = "用户名长度在3~16个字符")
private String username;
private String password;
// 自定义身份证号码验证*
***@NotBlank(message = "身份证号码不能为空")
@IdCard(message = "身份证格式不正确")
private String idCard;
}

2.6.6 postman测试,结果如下:

 

 

2.7 方案5:hibernate-validator校验器,同一个业务实体,如果不同场景下,校验的不一致属性****

2.7.1 定义分组接口,如下:

public interface Add {
}

 

public interface Update {
}

2.7.2 参数实体的属性,加上类似groups = Update.class,如下:

@Data
public class SysUserDTO implements Serializable {

    @NotNull(message = "id不能为空", groups = Update.class)
private Integer id;

    @NotBlank(message = "用户名不能为空")
@Length(min = 3,max = 16,message = "用户名长度在3~16个字符")
private String username;
private String password;

     // 自定义身份证号码验证*
***@NotBlank(message = "身份证号码不能为空")
@IdCard(message = "身份证格式不正确")
private String idCard;
}

2.7.3 在业务的方法中,调用ValidatorUtil.validate(参数,Update.class);****

@PostMapping("/update")
public RespResult updateSysUser(@RequestBody SysUserDTO sysUserDTO){
// 调用自定义异常处理类*
***ValidatorUtil.validate(sysUserDTO, Update.class);

    // 分别演示测试,观察现象

// ValidatorUtil.validate(sysUserDTO, Add.class);
return RespResult.success(sysUserDTO);
}