优雅的在spring 中使用 Hibernate Validator

校验

校验这个问题,在程序中到处都是存在的,而且是非常多的地方都需要校验,校验是为了数据完整性,正确性。通常我们在很多地方都会进行校验的,什么非空性校验,长度校验等等,时刻存在。

if (null == result || CollectionUtils.isEmpty(result.getData())) {
            log.warn("error.....";
         
 }

这样的代码时刻都是存在的,当然这样处理起来也是比较简单方便的,有自己适合的场景需要,很多通用性的校验让Hibernate Validator这样的框架产生了价值,spring mvc中也是集成了这个,但是给我的感觉使用起来还是不是非常的方便,干脆自己编写一个aop去处理,但是处理的逻辑都是类似的,没有什么好说的。

一、原始spring mvc 使用的校验

1、通过BindResult 进行校验Bean

将@Validated 可以携带分组信息,@Valid 校验Bean的信息
在spring mvc controller处理的场景中并不是根据aop进行处理的,而且通过参数注入的过程中进行校验处理,然后调用校验API 校验Bean的信息,这样的处理后面必须跟一个BindingResult ,获取错误的信息。如果没有,会抛出 MethodArgumentNotValidException这样的异常,我个人是不喜欢这样的强制让我跟一个后缀类的信息,感觉没有什么用,每次都要添加相同的代码。

@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid {
}
**
 * Variant of JSR-303's {@link javax.validation.Valid}, supporting the
 * specification of validation groups. Designed for convenient use with
 * Spring's JSR-303 support but not JSR-303 specific.
 *
 * <p>Can be used e.g. with Spring MVC handler methods arguments.
 * Supported through {@link org.springframework.validation.SmartValidator}'s
 * validation hint concept, with validation group classes acting as hint objects.
 *
 * <p>Can also be used with method level validation, indicating that a specific
 * class is supposed to be validated at the method level (acting as a pointcut
 * for the corresponding validation interceptor), but also optionally specifying
 * the validation groups for method-level validation in the annotated class.
 * Applying this annotation at the method level allows for overriding the
 * validation groups for a specific method but does not serve as a pointcut;
 * a class-level annotation is nevertheless necessary to trigger method validation
 * for a specific bean to begin with. Can also be used as a meta-annotation on a
 * custom stereotype annotation or a custom group-specific validated annotation.
 *
 * @author Juergen Hoeller
 * @since 3.1
 * @see javax.validation.Validator#validate(Object, Class[])
 * @see org.springframework.validation.SmartValidator#validate(Object, org.springframework.validation.Errors, Object...)
 * @see org.springframework.validation.beanvalidation.SpringValidatorAdapter
 * @see org.springframework.validation.beanvalidation.MethodValidationPostProcessor
 */
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {

	/**
	 * Specify one or more validation groups to apply to the validation step
	 * kicked off by this annotation.
	 * <p>JSR-303 defines validation groups as custom annotations which an application declares
	 * for the sole purpose of using them as type-safe group arguments, as implemented in
	 * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
	 * <p>Other {@link org.springframework.validation.SmartValidator} implementations may
	 * support class arguments in other ways as well.
	 */
	Class<?>[] value() default {};

}

优雅的在spring 中使用 Hibernate Validator

@PostMapping("/")
    @ResponseBody
    public String checkPersonInfo(@Valid @RequestBody PersonForm personForm,BindingResult bindingResult) {

         if (bindingResult.hasErrors()) {
          return bindingResult.getAllErrors().toString();
         }

        return "ok";
   }

2、使用 MethodValidationPostProcessor 这个AOP

这个AOP只处理类上放置@Validated 注解,可以放置Hibernate Validator分组信息,具体可以了解官方文档,
根据文档说明可以知道这个可以处理基本的参数校验,还能处理 void hello(@Valid PersonForm personFor,@NotBlank(message = “参数不能为空#520”) String text) 这样的校验,对于参数非参数都是支持的,对于需要使用国际化的,自己查看配置吧!校验Bean内部,校验参数注解,aop全搞定,这个会抛出 ConstraintViolationException异常。
优雅的在spring 中使用 Hibernate Validator

使用首先在类上添加一个@Validated,如果有分组信息,就近原则,method中有,使用method的分组,否则使用类上的,然后没有使用默认的Default

    @PostMapping("/test")
    @ResponseBody
    public String test(@NotBlank(message = "参数不能为空#520")  String text) {

        pagoTest.checkPersonInfo(new PersonForm());

        return "ok";
    }

这里对于两种基本注解校验和基本的参数校验都使用了…自己随便找个spring boot工程都行哈哈!优雅的在spring 中使用 Hibernate Validator

二、我喜欢的风格

1、自己喜欢的风格

对于我自己经常喜欢写 ,如果这个为空,抛出异常吧!自己不想去捕获,但是…不是不捕获,有一个地方处理就行了…

 if (StringUtils.isBlank(token)) {
            BizExceptionsUtil.create(ErrorCodeEnum.PARAMETER_ERROR);
  }

2、有点反感的风格

一般Controller中各种都有业务异常,都要在Controller这里添加各种Controller进行处理,非常的烦,各种无效的代码!

优雅的在spring 中使用 Hibernate Validator

三、优雅的使用校验

什么是优雅的使用,无论是各种RPC,dubbo、hsf 框架还是spring mvc ,Servlet 中的filter等等,总是有地方给你拦截处理的,统一的去处理各种异常,不需要在每一个方法中去进行处理,总体的思想就是责任链模式处理。

1、快速失败模式配置,又一个错误就停止校验

import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

import javax.validation.Validation;
import javax.validation.Validator;

/**
 * 设置快速失败模式
 * 
 * @author wangji
 */
@Configuration
public class HibernateValidatorConfig {

    @Bean
    @Primary
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
        postProcessor.setBeforeExistingAdvisors(true);
        postProcessor.setProxyTargetClass(true);
        postProcessor.setValidator(validator());
        return postProcessor;
    }

    /**
     * 开启快速失败模式,一旦失败立即抛出异常
     * 
     * @return
     */
    @Bean
    @Primary
    public Validator validator() {
        return Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
    }
}

2、全局异常捕获,这里针对 spring mvc的controller处理

这样上面的原始使用 Hibernate Validator中的错误,都可以在这里处理@ControllerAdvice 这个是很有意思的一个注解,对于全局异常捕获很奇妙,可以看看源码自己学习一下。MethodArgumentNotValidException,ConstraintViolationException这两个处理,我们在Controller中就不需要处理异常的信息啦,让代码干净整洁。

/**
 * 全局异常处理(可以参考 {@link org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler}) 由于在处理中
 * MethodArgumentNotValidException 和上面冲突不能继承这个类
 * 
 * @author wangji
 */
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
  
    STATE_CODE_DIVIDER="#";

    @ExceptionHandler({ Exception.class })
    public ActionResult<String> handleCommonException(Exception e) {
        log.error("捕获 Exception 异常", e);
        return ActionResult.getErrorResult("系统错误");
    }
		//各种业务异常统一处理  
    @ExceptionHandler({ BizException.class })
    public ActionResult<String> handleBizException(BizException ex) {
        log.info("捕获异常信息", ex);
        return BizExceptionsUtil.parseBizExpection(ex);
    }

    /**
     * {@link org.springframework.validation.annotation.Validated} {@link javax.validation.Valid} spring mvc controller中
     * 校验 Bean信息 使用上面两个都可以的,后面跟上一个BindResult,这样非常不方便,统一起来处理
     * 
     * @param e
     * @return
     */
    @ExceptionHandler({ MethodArgumentNotValidException.class })
    public ActionResult<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        ActionResult<String> actionResult = new ActionResult<>(false);
        BindingResult result = e.getBindingResult();
        FieldError error = result.getFieldError();
        String message = error.getDefaultMessage();
        log.debug("validator error filed={},message={} ", error.getField(), error.getDefaultMessage(), e);
        parseMessageAndCodeFromValidatorError(actionResult, message);
        return actionResult;
    }

    /**
     * {@link org.springframework.validation.annotation.Validated}
     * {@link org.springframework.validation.beanvalidation.MethodValidationPostProcessor}
     * 扫描类上含有Validated注解的类,含有校验不通过抛出异常 {@link org.springframework.validation.beanvalidation.MethodValidationInterceptor}
     * 当前类是主要的处理这,可以了解Hibernate Validator的一些用法
     * 
     * @param e
     * @return
     */
    @ExceptionHandler({ ConstraintViolationException.class })
    public ActionResult<String> handleMethodArgumentNotValidException(ConstraintViolationException e) {
        log.debug("validator error ", e);
        ActionResult<String> actionResult = new ActionResult<>(false);
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        for (ConstraintViolation item : constraintViolations) {
            log.debug("validator error:" + item.getPropertyPath().toString() + "  message:" + item.getMessage());
            parseMessageAndCodeFromValidatorError(actionResult, item.getMessage());
            break;
        }
        return actionResult;
    }

    /**
     * 从校验信息中解析meesage and code;eg:message = "参数错误 ||202"
     * 
     * @param actionResult
     * @param message
     */
    private void parseMessageAndCodeFromValidatorError(ActionResult<String> actionResult, String message) {
        if (message.indexOf(SanConstant.STATE_CODE_DIVIDER) > 0) {
            String[] messages = message.split(STATE_CODE_DIVIDER);
            actionResult.setMessage(messages[0]);
            actionResult.setCode(messages[1]);
        } else {
            actionResult.setMessage(message);
        }
    }

}

3、一般全局都有统一的返回信息结构,code ,message等等

可以在异常中message 中使用分割符处理,简单的手动分割字符:比如 参数错误#505 等等
优雅的在spring 中使用 Hibernate Validator

四、总结

没事多总结,发现一些奇妙的事情,之前自己封装的aop虽然也可以用,可能是自己之前没有处理好spring mvc原生的使用的问题吧,其实原生也是处理的那些场景的。

相关文章: