数据校验之Spring和Hibernate validate

原文链接: https://juejin.im/post/5a4ef30051882573351a1a12

title: 数据校验之Spring和Hibernate validate tags:

  • spring
  • Hibernate Validator categories: spring date: 2017-06-25 18:18:56

目前系统中使用校验的地方比较多,前端校验&后端校验是绕不开的两个话题。

通常来说对于开发来说:后端校验是必备,前端校验是可选

1.前端验证可以没有,但后端验证必须要有。

原因:(1)可以通过某些工具绕过前端验证,后端验证是保证数据有效性的防线。

     (2)前端验证有局限性。例如身份证号,需要调用api,需要后端进行验证。

2.前端验证也是必要的,可以优化用户的体验

前端验证不用提交数据,可以较快给出相应提示,用户体验比较好。

对于传统使用springmvc的项目通常可以使用spring validate来进行(实质还是使用hibernate validate)

那么具体分析一下代码。

正常使用springMVC项目通过注解Valid来实现校验

负责参数注入的实现类为

    ErrorsMethodArgumentResolver 该参数解析器负责所有Errors接口的参数(一般使用BindResult)

    public Object resolveArgument(
          MethodParameter parameter, ModelAndViewContainer mavContainer,
          NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
          throws Exception {
     
       ModelMap model = mavContainer.getModel();
       if (model.size() > 0) {
          int lastIndex = model.size()-1;
          String lastKey = new ArrayList(model.keySet()).get(lastIndex);
          if (lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
             return model.get(lastKey);
          }
       }
     
       throw new IllegalStateException(
             "An Errors/BindingResult argument is expected to be declared immediately after the model attribute, " +
             "the @RequestBody or the @RequestPart arguments to which they apply: " + parameter.getMethod());
    }
复制代码

通常来说使用spring需要声明对应的validator

比如

    <bean id="validateMessageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="classpath:validateMessages"/>
        <property name="defaultEncoding" value="utf-8"/>
    bean>
    
    <bean id="validator"
          class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
        <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
        
        <property name="validationMessageSource" ref="validateMessageSource"/>
    bean>
复制代码

通常为了支持i18n还会声明对应的LocaleResolver

    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
        <property name="cookieName" value="ln"/>
        <property name="cookieMaxAge" value="#{3*60*60*1000}"/>
        <property name="defaultLocale" value="zh"/>
    bean>
复制代码

这样我们可以根据用户的cookie来定义用户的语言(虽然目前都是中文)

我们在校验对应的controler参数时在代码参数上增加Valid注解

    else if (paramAnn.annotationType().getSimpleName().startsWith("Valid")) {
       validate = true;
       Object value = AnnotationUtils.getValue(paramAnn);
       validationHints = (value instanceof Object[] ? (Object[]) value : new Object[] {value});
    }
复制代码

很明显代码中一旦方法注解包含Valid作为起始那么校验模块开启

    WebDataBinder binder =
          resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler);
    boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
    if (binder.getTarget() != null) {
       doBind(binder, webRequest, validate, validationHints, !assignBindingResult);
    }
    args[i] = binder.getTarget();
    if (assignBindingResult) {
       args[i + 1] = binder.getBindingResult();
       i++;
    }
    implicitModel.putAll(binder.getBindingResult().getModel());
复制代码

判断下一个参数是否是Errors接口如果下一个参数是Errors接口就执行校验

    private void doBind(WebDataBinder binder, NativeWebRequest webRequest, boolean validate,
          Object[] validationHints, boolean failOnErrors) throws Exception {
     
       doBind(binder, webRequest);
       if (validate) {
          binder.validate(validationHints);
       }
       if (failOnErrors && binder.getBindingResult().hasErrors()) {
          throw new BindException(binder.getBindingResult());
       }
    }
复制代码

校验对应的参数同时处理封装对应的bindResult

    protected void processConstraintViolations(Set> violations, Errors errors) {
       for (ConstraintViolation violation : violations) {
          String field = violation.getPropertyPath().toString();
          FieldError fieldError = errors.getFieldError(field);
          if (fieldError == null || !fieldError.isBindingFailure()) {
             try {
                ConstraintDescriptor cd = violation.getConstraintDescriptor();
                String errorCode = cd.getAnnotation().annotationType().getSimpleName();
                Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd);
                if (errors instanceof BindingResult) {
                   // Can do custom FieldError registration with invalid value from ConstraintViolation,
                   // as necessary for Hibernate Validator compatibility (non-indexed set path in field)
                   BindingResult bindingResult = (BindingResult) errors;
                   String nestedField = bindingResult.getNestedPath() + field;
                   if ("".equals(nestedField)) {
                      String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
                      bindingResult.addError(new ObjectError(
                            errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()));
                   }
                   else {
                      Object invalidValue = violation.getInvalidValue();
                      if (!"".equals(field) && (invalidValue == violation.getLeafBean() ||
                            (field.contains(".") && !field.contains("[]")))) {
                         // Possibly a bean constraint with property path: retrieve the actual property value.
                         // However, explicitly avoid this for "address[]" style paths that we can't handle.
                         invalidValue = bindingResult.getRawFieldValue(field);
                      }
                      String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
                      bindingResult.addError(new FieldError(
                            errors.getObjectName(), nestedField, invalidValue, false,
                            errorCodes, errorArgs, violation.getMessage()));
                   }
                }
                else {
                   // got no BindingResult - can only do standard rejectValue call
                   // with automatic extraction of the current field value
                   errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
                }
             }
             catch (NotReadablePropertyException ex) {
                throw new IllegalStateException("JSR-303 validated property '" + field +
                      "' does not have a corresponding accessor for Spring data binding - " +
                      "check your DataBinder's configuration (bean property versus direct field access)", ex);
             }
          }
       }
    }
复制代码 
   

可以看到 通过调用validateImpl(hibernate)获取对应的失败约束,同时将对应的值和message等等信息封装

    if (!attrName.startsWith(BindingResult.MODEL_KEY_PREFIX) &&
          (isSessionAttr || isBindingCandidate(attrValue))) {
       String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + attrName;
       if (mavModel != null && !model.containsKey(bindingResultKey)) {
          WebDataBinder binder = createBinder(webRequest, attrValue, attrName);
          initBinder(handler, attrName, binder, webRequest);
          mavModel.put(bindingResultKey, binder.getBindingResult());
       }
    }
复制代码

当执行某些ArgumentResolver

    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
          NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
     
       Object argument = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
     
       String name = Conventions.getVariableNameForParameter(parameter);
       WebDataBinder binder = binderFactory.createBinder(webRequest, argument, name);
     
       if (argument != null) {
          validate(binder, parameter);
       }
     
       mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
     
       return argument;
    }
复制代码

明显modelAndView会将当前的bindingResult放入最后

而Errors获取参数

    public Object resolveArgument(
          MethodParameter parameter, ModelAndViewContainer mavContainer,
          NativeWebRequest webRequest, WebDataBinderFactory binderFactory)
          throws Exception {
     
       ModelMap model = mavContainer.getModel();
       if (model.size() > 0) {
          int lastIndex = model.size()-1;
          String lastKey = new ArrayList(model.keySet()).get(lastIndex);
          if (lastKey.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
             return model.get(lastKey);
          }
       }
     
       throw new IllegalStateException(
             "An Errors/BindingResult argument is expected to be declared immediately after the model attribute, " +
             "the @RequestBody or the @RequestPart arguments to which they apply: " + parameter.getMethod());
    }
复制代码

将末尾的参数获取,换言之 BindResults参数必须接在加了@Valid的相关某一个参数之后。

那么就可以成功的将对应的bindingResult赋值到指定的参数上。

你可能感兴趣的:(数据校验之Spring和Hibernate validate)