一、 常见场景
在日常的开发中,参数校验是非常重要的一个环节,严格参数校验会减少很多出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、自定义校验器
测试校验