@Valid和@Validator, hibernate-validator校验多重嵌套dto参数不生效的问题解决

一. 关于hibernate-validator

这个是针对mvc参数校验的一个框架,不过相信大家看这个文章的自然是知道是做什么用的。可以看我的这篇使用hibernate-validator做参数校验的文章

二. 嵌套实体校验不生效

不过@Valid和@Validator针对多重嵌套的dto没办法很好的校验

比如A类, 有个字段是List<B> bs, 我们可以用@Valid和@Validator去校验A类中的字段, 比如@NotEmpty, @NotNull等。 但是会发现B类中加了这些注解不生效。

在网上搜索的结果是,在List<B> bs上加注解@Valid, 在A上加注解@Validator。 确实可行, 但是@Valid没有group分组功能。比如:

public void save(@Validator(SaveGroup.class) @RequestBody A a){}

public void update(@Validator(UpdateGroup.class) @RequestBody A a){}

public class A{
...
	@Valid
	private List<B> bs;

}


public class B{
 	@NotNull(UpdateGroup.class)
 	private Integer id;
 	@NotNull(SaveGroup.class)
 	private String name;
}

如上所示, 我们会发现, 虽然在List<B>上加了注解能让B中的校验生效, 但是分组功能却无法使用. 我们想要在save的时候判断name不为空, update的时候判断id不为空.

三. 拦截器方案解决

这里使用拦截器来改造下这个校验功能。hibernate-validator提供了工具类让我们能自己校验参数:

ValidatorUtils


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

/**
 * hibernate-validator校验工具类
 *
 * 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
 *
 * @author zzzgd
 */
public class ValidatorUtils {
  private static Validator validator;

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

  /**
   * 校验对象
   * @param object        待校验对象
   * @param groups        待校验的组
   * @throws BusinessException  校验不通过,则报RRException异常
   */
  public static void validateEntity(Object object, Class<?>... groups)
          throws BusinessException {
    Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
    if (!constraintViolations.isEmpty()) {
      ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
      throw new BusinessException(constraint.getMessage());
    }
  }
}


自定义注解

InnerValidator

/**
 * InnerValidator
 *
 * @author zgd
 * @date 2020/9/27 16:38
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerValidator {

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

}

拦截器

ParamInterceptor

package com.zzzgd.aop.interceptor;

import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import com.zzzgd.valid.ValidatorUtils;
import com.zzzgd.anno.InnerValidator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodParameter;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ParamInterceptor
 *
 * @author zgd
 * @date 2020/9/27 16:33
 */
public class ParamInterceptor implements HandlerInterceptor, InitializingBean {

  @Autowired
  private RequestMappingHandlerAdapter adapter;
  private List<HandlerMethodArgumentResolver> argumentResolvers;
  // 缓存
  private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
  private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);

  /**
   * 这里换成自己的代码的基本包名,只校验这个包下的dto
   */
  private final static String BASE_PACKAGE = "com.zzzgd";

  @Override
  public void afterPropertiesSet() {
    argumentResolvers = adapter.getArgumentResolvers();
  }

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    // 只处理HandlerMethod方式
    if (handler instanceof HandlerMethod) {
      HandlerMethod method = (HandlerMethod) handler;
      // 拿到该方法所有的参数们~~~  org.springframework.core.MethodParameter
      MethodParameter[] parameters = method.getMethodParameters();
      if (parameters.length == 0) {
        return true;
      }

      //遍历所有的入参:给每个参数做赋值和数据绑定
      for (MethodParameter parameter : parameters) {
        Valid valid = parameter.getParameterAnnotation(Valid.class);
        Validated validated = parameter.getParameterAnnotation(Validated.class);
        if (valid == null && validated == null) {
          continue;
        }
        Class<?>[] groups = {};
        if (validated != null) {
          groups = validated.value();
        }

        Object value = getValue(parameter, request, response, method);
        validInnerField(parameter.getParameterType(), value, groups);
      }
    }
    return true;

  }

  private void validInnerField(Class clazz, Object value, Class<?>[] groups) {
    if (value == null || !isCustomClass(clazz)) {
      //如果值是null无需嵌套实体dto,或者类型是jdk自带类型,不可能在jdk自带类型的字段上加校验参数的注解
      return;
    }
    Field[] fields = ReflectUtil.getFields(clazz);
    for (Field field : fields) {
      Object fieldValue = ReflectUtil.getFieldValue(value, field);
      Class<?> fieldType = field.getType();
      Type genericType = field.getGenericType();
      if (!isCustomClass(getSuperClassGenricType(genericType))) {
        //如果字段类型是 jdk自带类型,也不符合嵌套dto,并且允许在字段上加注解的情况
        continue;
      }
      InnerValidator annotation = AnnotationUtil.getAnnotation(field, InnerValidator.class);

      //校验该字段的值
      if (fieldValue != null) {

        if (annotation != null) {
          Class<?>[] groups2 = annotation.value();
          //如果该字段上的InnerValidator有标注分组,则按远近原则, 优先以这个分组为准
          groups = groups2.length == 0 ? groups : groups2;

        }

        if (fieldValue instanceof Iterable) {
          //是集合list
          for (Object ov : ((Iterable) fieldValue)) {
            fieldType = ov.getClass();
            if (annotation != null) {
              //校验其中的值
              ValidatorUtils.validateEntity(ov, groups);
            }
            //递归判断这个字段实体中,是否还有这个注解
            validInnerField(fieldType, ov, groups);
          }
        } else {
          if (annotation != null) {
            //校验其中的值
            ValidatorUtils.validateEntity(fieldValue, groups);
          }
          validInnerField(fieldType, fieldValue, groups);
        }
      }
    }
  }

  /**
   * 判断一个类是JAVA类型还是用户定义类型
   *
   * @param clz
   * @return
   */
  public static boolean isJdkClass(Class<?> clz) {
    return clz != null && clz.getClassLoader() == null;
  }
  public static boolean isCustomClass(Class<?> clz) {
    return clz != null && clz.getName().startsWith(BASE_PACKAGE);
  }

  private Object getValue(MethodParameter parameter, HttpServletRequest request, HttpServletResponse response, HandlerMethod method) throws Exception {
    // 找到适合解析这个参数的处理器~
    HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
    Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");

    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));

    WebDataBinderFactory webDataBinderFactory = getDataBinderFactory(method);
    return resolver.resolveArgument(parameter, mavContainer, new ServletWebRequest(request, response), webDataBinderFactory);
  }





  private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
    Class<?> handlerType = handlerMethod.getBeanType();
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
      // 支持到@InitBinder注解
      methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS);
      this.initBinderCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    for (Method method : methods) {
      Object bean = handlerMethod.getBean();
      initBinderMethods.add(new InvocableHandlerMethod(bean, method));
    }
    return new ServletRequestDataBinderFactory(initBinderMethods, adapter.getWebBindingInitializer());
  }


  private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
    HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
    if (result == null) {
      for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
        if (methodArgumentResolver.supportsParameter(parameter)) {
          result = methodArgumentResolver;
          this.argumentResolverCache.put(parameter, result);
          break;
        }
      }
    }
    return result;
  }

  public static Class getSuperClassGenricType(Type type) throws IndexOutOfBoundsException {
    if (!(type instanceof ParameterizedType)) {
      return Object.class;
    }

    Type[] params = ((ParameterizedType) type).getActualTypeArguments();

    if (params.length == 1) {
      return (Class) params[0];
    }
    for (Type param : params) {
      if (!(param instanceof Class)) {
        return Object.class;
      }
    }
    return Object.class;
  }

}

配置拦截器

WebMvcConfig

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  // 配置用于校验的拦截器
  @Bean
  public ParamInterceptor validationInterceptor() {
    return new ParamInterceptor();
  }
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(validationInterceptor()).addPathPatterns("/**");
  }
}

三. 使用

  1. 可以修改拦截器中validInnerField方法中, 是调用isCustomClass还是isJdkClass, 前者指定包路径, 只校验这个包下面的dto, 后者只校验非jdk的类。因为我们用来加@NotEmpty, @NotNull的这些类肯定不是这些class
  2. 在Controller方法中,对最外层的dto加@Valid或者@Validator注解, 那么这个dto中有@InnerValidator注解的字段都会被校验。比如上面的A类和B类,甚至B类有个字段List<C>,那么只要在List<C>字段上加@InnerValidator注解即可
  3. 分组也是可以沿用的。比如Controller方法中@Validator注解写明了分组是SaveGroup,那么其中嵌套的@InnerValidator注解的字段也会按这个分组校验,也可以在@InnerValidator注解中标特定的分组类。
  4. 目前初步是用这个来实现,后续有优化或修复会同步更新
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值