如何优雅实现统一参数的校验?

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

二、实现方案
1、集成hibernate-validator框架,基于框架提供的注解实现数据验证(应用比较广泛,与springboot集成比较好,推荐)
pom.xml中引入

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.17.Final</version>
</dependency>

2、集成oval框架,同样基于框架提供的注解实现数据验证
pom.xml中引入

<dependency>
    <groupId>net.sf.oval</groupId>
    <artifactId>oval</artifactId>
    <version>1.90</version>
</dependency>

三、实现步骤
1、基于hibernate-validator实现,有两种实现方式:
1.1 使用springboot validator框架自带的注解实现数据验证。
1.1.1 在参数实体中加类似注解,类似@NotBlank,@NotEmpty,@NotNull,@Range,示例代码如下:

@Data
@EqualsAndHashCode(callSuper = false)
public class SysBlackListAddOrEditDTO extends SysBlackListCommonDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @NotBlank(message = "id不能为空", groups = Update.class)
    private String id;
}

1.1.2 在controller控制层方法的参数那里加上@Validated,示例代码如下:

    @ApiOperation(value = "新增黑名单保存", notes = "新增黑名单保存")
    @PostMapping("add")
    @Lock(keyPrefix = "mybatis/actinfo", lockFailMsg = LockConstant.REPEATED_SUBMIT, value = "#req.reqUrl")
    public RespResult<Boolean> add(@Validated @RequestBody SysBlackListAddOrEditDTO req) {
        log.info("进入新增黑名单....");
        return sysBlackListService.save(req);
    }

1.2 开发一个验证的工具类ValidatorUtil.java, 提供校验参数的方法,抛出ParamException运行时异常(推荐,比较优雅),代码如下:
1.2.1 在参数实体属性中加类似校验注解,类似@NotEmpty,@NotNull,@Min @Max @Range,@Pattern 例如:
@Range(min = 12, max = 60, messsage= "年龄必须在12到60之间")
private Integer age;
1.2.2 在业务的方法中,调用ValidatorUtil.validate(req)
1.2.3 定义封装好的业务异常ParamException,继承运行时异常
1.3 定义通过全局异常拦截器GlobalExceptionHandler拦截ParamException异常,返回参数错误的异常code和message,代码如下:

@ExceptionHandler(MethodArgumentNotValidException.class)
public RespResult<Void> 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);
}

四、如何自定义hibernate-validator校验器?
一般情况,内置的校验器可以解决很多问题。但也有无法满足情况的时候,此时,我们可以实现validator的接口,自定义自己需要的验证器。
1)与普通注解相比,这种自定义注解需要增加元注解@Constraint,并通过validatedBy参数指定验证器。
2)依据JSR规范,定义三个通用参数:message(校验失败保存信息)、groups(分组)和payload(负载)
3)定义一个参数验证器,实现参数校验的逻辑
例如:自定义一个IdCard身份证校验的注解、IdCard自定义校验器

五、hibernate-validator校验器,同一个业务实体,如果不同场景下,校验的属性不一致,比如在新增的时候业务实体中的ID不需要必填,但是在修改操作的时候,ID是必填的,这个时候就可以使用group进行分组操作
1)定义分组接口,例如:
public interface Add {
}
public interface Update {
}
2)参数实体的属性,加上类似groups = Add.class,例如:
@NotEmpty(message = "id不能为空", groups = Update.class)
3)在业务的方法中,调用ValidatorUtil.validate(req,Update.class);

六、代码演示

1、使用springboot validator框架自带的注解实现

controller层

package com.litian.dancechar.core.biz.blacklist.controller;
import com.litian.dancechar.core.biz.blacklist.conf.EnvConfig;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListAddOrEditDTO;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListReqDTO;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListRespDTO;
import com.litian.dancechar.core.biz.blacklist.service.SysBlackListService;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.validator.ValidatorUtil;
import com.litian.dancechar.framework.common.validator.groups.Update;
import com.litian.dancechar.framework.cache.redis.constants.LockConstant;
import com.litian.dancechar.framework.cache.redis.distributelock.annotation.Lock;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;

/**
 * 黑名单地址业务处理
 *
 * @author kyle0432
 * @date 2024/03/09 11:17
 */
@Api(tags = "黑名单相关api")
@RestController
@Slf4j
@RequestMapping(value = "/sys/blacklist/")
public class SysBlackListController {

    @Resource
    private SysBlackListService sysBlackListService;

    @ApiOperation(value = "新增黑名单保存", notes = "新增黑名单保存")
    @PostMapping("add")
    @Lock(keyPrefix = "mybatis/actinfo", lockFailMsg = LockConstant.REPEATED_SUBMIT, value = "#req.reqUrl")
    public RespResult<Boolean> add(@Validated @RequestBody SysBlackListAddOrEditDTO req) {
        log.info("进入新增黑名单....");
        return sysBlackListService.save(req);
    }

}

入参实体类

package com.litian.dancechar.core.biz.blacklist.dto;

import com.litian.dancechar.framework.common.validator.groups.Update;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
 * 黑名单-新增或修改DTO
 *
 * @author kyle0432
 * @date 2024/03/09 11:27
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class SysBlackListAddOrEditDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 主键,这里使用groups分组,表示的是在当前这个属性,如果是修改操作id是必填的
     */
    @NotBlank(message = "id不能为空", groups = Update.class)
    private String id;

    /**
     * 来源(1-管理后台  2-前端应用)
     */
    @NotBlank(message = "来源不能为空")
    private String source;

    /**
     * 请求url
     */
    @NotBlank(message = "请求url不能为空")
    private String reqUrl;

    /**
     * 链接名称
     */
    private String blackName;

    /**
     * 手机号
     */
    @DecryptField(value = EncryptTypeEnum.MOBILE)
    private String mobile;

    /**
     * 备注
     */
    private String remark;

    /**
     * 渠道列表
     */
    @Valid
    @NotNull(message = "渠道列表不能为空")
    private List<ChannelDTO> channelList;
}

创建分组的新增和更新接口

/**
 * 分组-更新
 *
 * @author kyle0432
 * @date 2024/03/09 12:30
 */
public interface Update {
}
/**
 * 分组-新增
 *
 * @author kyle0432
 * @date 2024/03/09 12:30
 */
public interface Add {
}

 全局统一异常处理类进行拦截非法校验参数

package com.litian.dancechar.framework.common.exception;

import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.base.RespResultCode;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;

/**
 * 全局异常类
 *
 * @author kyle0432
 * @date 2024/3/09 12:40
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public RespResult<Void> handleError(MethodArgumentTypeMismatchException e) {
        log.error("Method Argument Type Mismatch:{}", e.getMessage(), e);
        return RespResult.error("Method Argument Type Mismatch");
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public RespResult<Void> handleError(MissingServletRequestParameterException e) {
        log.error("Method Request Parameter :{}", e.getMessage(), e);
        return RespResult.error("Method Request Parameter");
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public RespResult<Void> 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);
    }

    @ExceptionHandler(BindException.class)
    public RespResult<Void> handleError(BindException e) {
        log.error("Bind Exception :{}", e.getMessage(), e);
        FieldError fieldError = e.getFieldError();
        String message = String.format("%s:%s", fieldError.getField(), fieldError.getDefaultMessage());
        return RespResult.error(RespResultCode.ERR_PARAM_NOT_LEGAL.getCode(), message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public RespResult<Void> handleError(ConstraintViolationException e) {
        log.error("Constraint Exception :{}", e.getMessage(), e);
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        ConstraintViolation<?> violation = violations.iterator().next();
        String path = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
        String message = String.format("%s:%s", path, violation.getMessage());
        return RespResult.error(RespResultCode.ERR_CONSTRAINT_VIOLATION.getCode(), message);
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public RespResult<Void> handleError(HttpMessageNotReadableException e) {
        log.error("Http Message Not Readable Exception :{}", e.getMessage(), e);
        return RespResult.error("请求的参数类型与方法接收的参数类型不匹配, 请检查!");
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public RespResult<Void> handleError(HttpRequestMethodNotSupportedException e) {
        log.error("Http Request Method Not Support Exception :{}", e.getMessage(), e);
        String message = String.format("%s方法类型不支持,请检查!", e.getMethod());
        return RespResult.error(message);
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public RespResult<Void> handleError(HttpMediaTypeNotSupportedException e) {
        log.error("Http Media Type Not Support Exception :{}", e.getMessage(), e);
        String message = String.format("不支持%s content-type,请检查!", e.getContentType());
        return RespResult.error(message);
    }

    @ExceptionHandler(ParamException.class)
    public RespResult<Void> 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(DistributeLockException.class)
    public RespResult<Void> handleError(DistributeLockException e) {
        log.error("分布式锁Distribute Lock Exception :{}", e.getMessage(), e);
        return RespResult.error(RespResultCode.REPEATED_OPERATE.getCode(),
                RespResultCode.REPEATED_OPERATE.getMessage(), e.getMessage());
    }

    @ExceptionHandler(BusinessException.class)
    public RespResult<Void> handleError(BusinessException e) {
        log.error("业务出现异常 :{}", e.getMessage(), e);
        return RespResult.error(RespResultCode.SYS_EXCEPTION.getCode(),
                RespResultCode.SYS_EXCEPTION.getMessage(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    //@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "系统内部错误")
    public RespResult<Void> handleError(Exception e) {
        log.error("全局异常信息,异常堆栈信息 :{}", e.getMessage(), e);
        return RespResult.error(RespResultCode.SYS_EXCEPTION.getCode(),
                RespResultCode.SYS_EXCEPTION.getMessage(), e.getMessage());
    }

    @ExceptionHandler(Throwable.class)
    public RespResult<Void> handleError(Throwable e) {
        log.error("Throwable Server Exception :{}", e.getMessage(), e);
        return RespResult.error(RespResultCode.SYS_EXCEPTION.getCode(),
                RespResultCode.SYS_EXCEPTION.getMessage(), e.getMessage());
    }
}

2、开发一个验证的工具类ValidatorUtil来进行实现

controller层

package com.litian.dancechar.core.biz.blacklist.controller;
import com.litian.dancechar.core.biz.blacklist.conf.EnvConfig;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListAddOrEditDTO;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListReqDTO;
import com.litian.dancechar.core.biz.blacklist.dto.SysBlackListRespDTO;
import com.litian.dancechar.core.biz.blacklist.service.SysBlackListService;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.validator.ValidatorUtil;
import com.litian.dancechar.framework.common.validator.groups.Update;
import com.litian.dancechar.framework.cache.redis.constants.LockConstant;
import com.litian.dancechar.framework.cache.redis.distributelock.annotation.Lock;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;

/**
 * 黑名单地址业务处理
 *
 * @author kyle0432
 * @date 2024/03/09 11:17
 */
@Api(tags = "黑名单相关api")
@RestController
@Slf4j
@RequestMapping(value = "/sys/blacklist/")
public class SysBlackListController {

    @Resource
    private SysBlackListService sysBlackListService;

    @ApiOperation(value = "修改黑名单保存", notes = "修改黑名单保存")
    @PostMapping("update")
    public RespResult<Boolean> update(@RequestBody SysBlackListAddOrEditDTO req) {
        log.info("进入修改黑名单....");
        ValidatorUtil.validate(req, Update.class);//这里表示的是分组操作为更新
        return sysBlackListService.save(req);
    }
}

校验器工具类ValidatorUtil

package com.litian.dancechar.framework.common.validator;

import com.litian.dancechar.framework.common.base.RespResultCode;
import com.litian.dancechar.framework.common.exception.ParamException;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * hibernate-validator校验工具类
 *
 * @author kyle0432
 * @date 2024/03/09 21:13
 */
public class ValidatorUtil {
    private static Validator validator;

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

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

3、自定义校验器实现

定义身份证参数校验器

package com.litian.dancechar.framework.common.validator.idcard;


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();
    }
}

定义注解,并且引用上面校验器

package com.litian.dancechar.framework.common.validator.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 {};
}

 入参实体类

package com.litian.dancechar.core.biz.blacklist.dto;

import com.litian.dancechar.framework.common.validator.idcard.IdCard;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

/**
 * 渠道DTO
 *
 * @author kyle0432
 * @date 2024/03/10 15:17
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class ChannelDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 渠道名称
     */
    @NotBlank(message = "渠道名称不能为空")
    @IdCard(message = "身份证格式不对")
    private String channelName;
}

七、结果演示

1、springboot validator框架方式

当必填参数为填写,校验错误如下

日志信息报错异常类型

全局异常类型进行捕获当前类型异常

2、ValidatorUtil方式实现

修改时去掉source必填参数

多个参数不合法,在响应结果对象中都会描述相关信息

控制台会抛出 ParamException异常,在工具类中进行拦截抛出的异常

最终会在全局统一异常类中进行拦截并且处理返回相应的信息

3、自定义校验器

测试校验

  • 30
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值