版权说明: 本文由博主keep丶原创,转载请注明出处。
原文地址: https://blog.csdn.net/qq_38688267/article/details/115720412
场景介绍
在定义接口参数对象时,我们需要通过@ApiModelProperty
注解声明字段信息,还需要通过@NotNull/@NotEmpty
等注解限制字段值信息,而这些校验字段都 需要填写message
属性来自定义校验失败后的消息内容,这个工作量比较大,重复工作也很多。如图:
因此,想要开发一个自定义的校验器或者自定义注解,实现根据@ApiModelProperty#value()
获取字段名,然后根据校验类型拼接描述字符,比如 @NotNull
注解拼接 “必填”、@NotEmpty
拼接 "不能为空"等
如果实现后则以后只需要加@NotNull/@NotEmpty
注解,不需要再一个个写@NotNull#message()
的值,可以减少很多重复的不必要的工作!
实现细节
校验器代理配置
@Validated
注解不仅能校验方法参数,还能校验返回值等多个功能。而我们暂时只需要重写其校验方法参数的功能,因此我们做的自定义校验还得兼容其他应用场景,因为有很多第三方jar也有用到@Validated
进行校验。
思来想去,尝试了几种方案后选择类似于多数据源的方案,通过一个代理类来实现多个校验器的路由,除了参数校验用自定义的校验器校验,其他的都使用默认的校验器校验。
部分代码如下:
public class IfmValidatorProxy implements Validator, ExecutableValidator {
private final Validator defaultValidator;
private final Validator customValidator;
public IfmValidatorProxy(Validator defaultValidator,
Validator customValidator) {
this.defaultValidator = defaultValidator;
this.customValidator = customValidator;
}
}
/**
* 配置一个校验器的代理类,自定义选择校验器进行校验
* <p>
* 这里必须加上@Primary注解,否则会跟{@link WebMvcConfigurationSupport#mvcValidator()}的冲突
*/
@Primary
@Bean
public Validator primaryValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
IfmValidator ifmValidator = new IfmValidator();
factoryBean.afterPropertiesSet();
return new IfmValidatorProxy(factoryBean.getValidator(), ifmValidator);
}
校验处理接口抽象
保证扩展性,我们抽象一个接口:
/**
* 校验注解处理抽象接口
* <p>
* ### 实现类要使用起来需要注册到{@link ValidateHandlerHelper}中 ###
*
* @author zzf
* @date 2021/4/15 9:22
*/
public interface ValidateAnnotationHandler<T extends Annotation> {
/**
* 获取实现类的具体的注解类对象
*/
Class<T> getAnnotation();
/**
* 判断参数字段是否存在该注解
*
* @param field 字段对象
* @return 是否存在该注解
*/
default Boolean isAnnotationPresent(Field field) {
return field.isAnnotationPresent(getAnnotation());
}
/**
* 校验字段值,如果值合法返回null,不合法返回消息内容
*
* @param field 需要校验的字段
* @param targetObject 字段所在的对象
* @return 值合法返回null,不合法返回消息内容
*/
default String validate(Field field, Object targetObject) {
T annotation = field.getAnnotation(getAnnotation());
return validate(annotation, ReflectUtil.getFieldValue(targetObject, field));
}
/**
* 校验字段值,如果值合法返回null,不合法返回消息内容
* <p>
* 这里之所以没有直接返回boolean而是直接返回msg
* 是为了适配如{@link javax.validation.constraints.Max}{@link javax.validation.constraints.Min}等
* 消息中需要获取注解属性值的注解
*
* @param validateAnnotation 标记的注解
* @param fieldValue 字段值
* @return 如果值合法返回null,不合法返回消息内容
*/
String validate(T validateAnnotation, Object fieldValue);
/**
* @return 消息模板
*/
String getResultMsgWhenInvalid();
}
校验处理器接口实现
直接贴代码啦:
public class MaxHandler implements ValidateAnnotationHandler<Max> {
@Override
public Class<Max> getAnnotation() {
return Max.class;
}
@Override
public String validate(Max validateAnnotation, Object fieldValue) {
long value = validateAnnotation.value();
boolean valid;
if (fieldValue instanceof Integer) {
valid = value > (Integer) fieldValue;
} else if (fieldValue instanceof BigDecimal | fieldValue instanceof Double | fieldValue instanceof Short) {
valid = new BigDecimal(String.valueOf(value)).compareTo(new BigDecimal(String.valueOf(fieldValue))) > 0;
} else if (fieldValue instanceof Long) {
valid = value > (Long) fieldValue;
} else if (fieldValue instanceof Date) {
valid = value > ((Date) fieldValue).getTime();
} else {
valid = true;
}
if (!valid) {
MessageFormatter.format(getResultMsgWhenInvalid(), value).getMessage();
}
return null;
}
@Override
public String getResultMsgWhenInvalid() {
return "的值必须小于{}!";
}
}
public class NotNullHandler implements ValidateAnnotationHandler<NotNull> {
@Override
public Class<NotNull> getAnnotation() {
return NotNull.class;
}
@Override
public String validate(NotNull validateAnnotation, Object fieldValue) {
if(ObjectUtil.isNull(fieldValue)) {
return getResultMsgWhenInvalid();
}
return null;
}
@Override
public String getResultMsgWhenInvalid() {
return "必传!";
}
}
参数校验类工厂
为了方便快速扩展和使用,包装了一个类似于简单工厂的对象:
public class ValidateHandlerHelper {
private static final Set<ValidateAnnotationHandler<?>> handlerSet = new HashSet<>(10);
/**
* 初始化注册校验处理类
*/
static {
handlerSet.add(new NotNullHandler());
handlerSet.add(new NotEmptyHandler());
handlerSet.add(new NotBlankHandler());
handlerSet.add(new MaxHandler());
handlerSet.add(new MinHandler());
}
/**
* 校验字段值,如果值合法返回null,不合法返回消息内容
*
* @param field 需要校验的字段
* @param targetObject 字段所在的对象
* @return 值合法返回null,不合法返回消息内容
*/
private static String validate(Field field, Object targetObject) {
Optional<ValidateAnnotationHandler<?>> handlerOptional =
handlerSet.stream().filter(s -> s.isAnnotationPresent(field)).findFirst();
if (handlerOptional.isPresent()) {
String validate = handlerOptional.get().validate(field, targetObject);
if (validate != null) {
String fieldComment;
if (field.isAnnotationPresent(ApiModelProperty.class)) {
ApiModelProperty annotation = field.getAnnotation(ApiModelProperty.class);
fieldComment = annotation.value();
} else {
fieldComment = field.getName();
}
return fieldComment + handlerOptional.get().getResultMsgWhenInvalid();
}
}
return null;
}
全局异常捕获
校验不通过的话,会抛出ConstraintViolationException
异常,我们捕获后返回给前端:
/**
* 参数校验异常
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ConstraintViolationException.class)
public ServiceResponse<?> handlerConstraintViolationException(ConstraintViolationException e) {
return new ServiceResponse<>(ResponseCodeEnum.PARAM_ERROR.getCode(), e.getMessage());
}
使用
添加校验标识注解
添加校验注解
在实现参数值校验时,我们做针对集合类型的字段做了递归处理,还可以深层校验集合元素属性哦~
效果展示
源码地址
- 项目地址:https://gitee.com/zengzefeng/easy-frame
- 相关博客:springcloud集成seata1.4.0,nacos1.4.0,sharding-jdbc,mybatis-plus实践
总结
该功能只是初版,肯定还有很多不足和考虑不周的地方,还请各位大佬多多指点。后续还会继续优化迭代。