重构校验逻辑可配置化

简介

  1. 我在读之前的老接口时,发现大量的校验耦合在服务层,导致我一时半会不知道里面要做什么?所以我想将校验逻辑与业务逻辑拆开,校验框架用的比较多的就是Hibernate-Validator
  2. 校验要考虑的事
    2.1 校验要有顺序
    2.2 校验中有些耗时操作(查库或者RPC调用),希望只调用一次,然后在不同的校验器中进行传播
    (1)可能后续的业务逻辑也需要
    2.3 同一个校验器可能会有不同的错误返回
  3. Hibernate-Validator的缺陷
    3.1 在不同的校验器中不能进行值传播
    3.2 貌似校验顺序也不固定(没有debug源码,调试现象观察)
  4. 为了解决Hibernate-Validator的缺陷,遇到了fluent-validator
    4.1 它的手动配置很好的解决上述问题,但spring-aop集成时,不能从返回值中拿到上下文传递的值,所以改造了一波(见改造后的版本简述段落)

Hibernate-Validator参数校验

  1. Spring参数校验示例
    1.1 @Valid 和 BindingResult 是一一对应的,如果有多个@Valid,那么每个@Valid后面跟着的BindingResult就是这个@Valid的验证结果,顺序不能乱
    1.2 如果此时去掉实体对象后面的BindingResult,如校验未通过会抛出BindException异常
    @RequestMapping
    @ResponseBody
    public Map<String, Object> main(@Validated OrderApiRequest orderApiRequest, BindingResult bindingResult){
        Map<String, Object> backMap = new HashMap<>();
        // 数据校验
        if (bindingResult.hasErrors()) {
            backMap.put(WebCst.MSG, bindingResult.getAllErrors().get(0).getDefaultMessage());
            return ResponseUtl.response(backMap, StatusCst.sys1);
        }
        return backMap;
    }
    
    @Data
    @ToString
    public class OrderApiRequest {
        @NotBlank(message = "content不能为空")
        private String content;
        @NotBlank(message = "token不能为空")
        private String token;
        @NotBlank(message = "sign不能为空")
    }
    
    1.3 Hibernate Validator有两种校验模式,默认为普通模式(会校验完所有的属性),快速失败返回模式(只要有一个字段验证失败,就返回结果)
    @Bean
    public Validator validator(){
        ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class)
                .configure()
                .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        return validator;
    }
    
  2. 原理分析
    2.1 在接受请求后,SpringMvc会将请求参数和方法参数进行绑定:InvocableHandlerMethod#getMethodArgumentValues
    	protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
    		Object... providedArgs) throws Exception {
    		MethodParameter[] parameters = getMethodParameters();
    		for (int i = 0; i < parameters.length; i++) {
    			MethodParameter parameter = parameters[i];
    			args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
    		}
    	}
    
    2.2 解析参数:ModelAttributeMethodProcessor#resolveArgument
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    		NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    	BindingResult bindingResult = null;
    	if (bindingResult == null) {
    		WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    		if (binder.getTarget() != null) {
    			// 校验请求参数(如果参数有Validated或者Valid注解,则DataBinder#validate)
    			validateIfApplicable(binder, parameter);
    		}
    	}
    }
    
    2.3 数据绑定校验
    在这里插入图片描述

分组校验

  1. 定义分组类,每个分组类只需要一个接口就可以了
    public interface OrderAddGroup {}
    
  2. 校验规则上添加分组
    @Data
    @ToString
    public class OrderApiRequest {
        @NotBlank(message = "content不能为空",groups = {OrderAddGroup.class})
        private String content;
    }
    
  3. 修改校验接口
    @RequestMapping
    @ResponseBody
    public Map<String, Object> main(@Validated(OrderAddGroup.class) OrderApiRequest orderApiRequest, BindingResult bindingResult){
    

ConstraintValidator注入问题

  1. 在配置Validator时,指定constraintValidatorFactory为SpringConstraintValidatorFactory,否则注入的bean为空
    在这里插入图片描述

自定义错误消息

public class CheckTokenValidator implements ConstraintValidator<CheckToken,String> {
	@Override
    public boolean isValid(String token, ConstraintValidatorContext constraintContext) {
    	.........
		if (null == customerTokenEnt || !customerTokenEnt.getToken().equals(token)) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate("用户验证失败").addConstraintViolation();
            return false;
        }
        if (1 != customerTokenEnt.getStatus().intValue()) {
            constraintContext.disableDefaultConstraintViolation();
            constraintContext.buildConstraintViolationWithTemplate("当前用户已停用,请联系客服人员").addConstraintViolation();
            return false;
        }
        return false;
	}
}

自定义上下文

  1. HibernateConstraintValidatorContext继承ConstraintValidatorContext(不是我想要的,烦)
    1.1 在自定义约束启用表达式插值
    1.2 使用hibernate econstraintvalidatorcontext#addMessageParameter(String, Object) 或 hibernate econstraintvalidatorcontext #addMessageParameter(String, Object)
    public class MyFutureValidator implements ConstraintValidator<Future, Instant> {
    	 HibernateConstraintValidatorContext hibernateContext = context.unwrap( HibernateConstraintValidatorContext.class );
    	 hibernateContext.disableDefaultConstraintViolation();
         hibernateContext
                 .addExpressionVariable( "now", now )
                 .buildConstraintViolationWithTemplate( "Must be after ${now}" )
                 .addConstraintViolation();
                 return false;
    }
    
  2. 看一下源码,上下文是怎么玩的?(SimpleConstraintTree#validateConstraints)
    2.1 要重新找突破口了
    protected <T> void validateConstraints(ValidationContext<T> validationContext,
    		ValueContext<?, ?> valueContext,
    		Set<ConstraintViolation<T>> constraintViolations) {
    	// find the right constraint validator (找到正确的校验器)
    	ConstraintValidator<B, ?> validator = getInitializedConstraintValidator( validationContext, valueContext );
    
    	// create a constraint validator context (创建上下文,窝草,居然是直接New的)
    	ConstraintValidatorContextImpl constraintValidatorContext = new ConstraintValidatorContextImpl(
    			validationContext.getParameterNames(),
    			validationContext.getClockProvider(),
    			valueContext.getPropertyPath(),
    			descriptor,
    			validationContext.getConstraintValidatorPayload()
    	);
    
    	// validate (校验)
    	constraintViolations.addAll(
    			validateSingleConstraint(
    					validationContext,
    					valueContext,
    					constraintValidatorContext,
    					validator
    			)
    	);
    }
    

fluent-validator校验

使用fluent-validator工具库

  1. 编写Validator
    //继承ValidatorHandler可以避免实现一些默认的方法
    public class CarSeatCountValidator extends ValidatorHandler<Integer> {
        @Override
        public boolean validate(ValidatorContext context, Integer t) {
            if (t != 2 && t != 5 && t != 7) {
            	//通过context放入错误消息并且返回false
                context.addErrorMsg(String.format(CarError.SEATCOUNT_ERROR.msg(), t));
                return false;
            }
            return true;
        }
    }
    
  2. 开始验证
    Car car = getCar();
    Result ret = FluentValidator.checkAll()   //获取了一个FluentValidator实例
                                .on(car.getLicensePlate(), new CarLicensePlateValidator())
                                .on(car.getManufacturer(), new CarManufacturerValidator())
                                .on(car.getSeatCount(), new CarSeatCountValidator())
                                .doValidate()  //真正执行验证
                                .result(toSimple());
    

设置回调函数

Result ret = FluentValidator.checkAll()
                .on(car.getSeatCount(), new CarSeatCountValidator())
                .doValidate(new DefaulValidateCallback() {
                     @Override
                     public void onSuccess(ValidatorElementList validatorElementList) {
                         LOG.info("all ok!");
                     }
                 }).result(toSimple());

集成Hibernate Validator

public class HiberateSupportedValidatorTest {
    //声明Hibernate Validator
	private static Validator validator;
	
    @BeforeClass
    public static void setUpValidator() {
        Locale.setDefault(Locale.ENGLISH);
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

	@Test
    public void testCompany() {
        Company company = CompanyBuilder.buildSimple();
        Result ret = FluentValidator.checkAll()
        			//HibernateSupportedValidator依赖于javax.validation.Validator的实现,也就是Hibernate Validator
	                .on(company, new HibernateSupportedValidator<Company>().setHiberanteValidator(validator))
	                .on(company, new CompanyCustomValidator())
	                .doValidate().result(toSimple());
	        System.out.println(ret);
	        assertThat(ret.isSuccess(), is(true));
	    }
}

注解验证

  1. @FluentValidate可以装饰在属性上,内部接收一个Class[]数组参数
    1.1 这些个classes必须是Validator的子类,这叫表明在某个属性上依次用这些Validator做验证
    public class Car {
    	@FluentValidate({CarManufacturerValidator.class})
    	private String manufacturer;
    }
    
  2. 进行验证
    @Test
    public void testCar() {
        Car car = getValidCar();
        Result ret = FluentValidator.checkAll().configure(new SimpleRegistry())
                .on(car)
                .doValidate()
                .result(toSimple());
        System.out.println(ret);
        assertThat(ret.isSuccess(), is(true));
    }
    

Spring 注解验证

  1. 你的验证器需要Spring IoC容器管理的bean注入,那么你干脆可以把Validator也用Spring托管,使用@Service或者@Component注解在Validator类上就可以做到
  2. 使用SpringApplicationContextRegistry
    2.1 作用:去Spring的容器中寻找Validator
  3. 引入相关依赖
    <dependency>
        <groupId>com.baidu.unbiz</groupId>
        <artifactId>fluent-validator-spring</artifactId>
        <version>1.0.5</version>
    </dependency>
    
注解验证源码分析
  1. FluentValidator#on方法分析
    protected <T> MultiValidatorElement doOn(final T t) {
    	// 获取Bean的所有待验证属性的验证列表
    	List<AnnotationValidator> anntValidatorsOfAllFields =
                AnnotationValidatorCache.getAnnotationValidator(registry, t);
        for (final AnnotationValidator anntValidatorOfOneField : anntValidatorsOfAllFields) {
        	// 反射获取属性的值
        	Object realTarget = ReflectionUtil.invokeMethod(anntValidatorOfOneField.getMethod(), t);
        	// 添加到elementList
        	 for (final Validator v : anntValidatorOfOneField.getValidators()) {
                    elementList.add(new ValidatorElement(realTarget, v, new ToStringable() {
                        @Override
                        public String toString() {
                            return String.format("%s#%s@%s", t.getClass().getSimpleName(),
                                    anntValidatorOfOneField.getField().getName(), v);
                        }
                    }));
                }
        }  
        MultiValidatorElement m = new MultiValidatorElement(elementList);
        // 看到这里,就和之前一样了(验证器链)
        validatorElementList.add(m);    
    }
    
  2. 根据注册器以及类获取所有需要被验证的属性
    private static List<AnnotationValidator> getAllAnnotationValidators(Registry registry, Class<?> clazz) {
    	// 获取所有包含指定<code>Annotation</code>的<code>Field</code>数组
    	// 遍历对象的字段,判断是否有FluentValidate注解
    	Field[] fields = ReflectionUtil.getAnnotationFields(clazz, FluentValidate.class);
    	// 遍历带FluentValidate注解的属性
    	for (int i = 0; i < fields.length; i++) {
    		Field field = fields[i];
    		FluentValidate fluentValidateAnnt = ReflectionUtil.getAnnotation(field, FluentValidate.class);
    		// 拿到FluentValidate 注解上的Validator类数组
    		Class<? extends Validator>[] validatorClasses = fluentValidateAnnt.value();
    		
    		 for (Class<? extends Validator> validatorClass : validatorClasses) {
    		 	 // 通过Registry查找Validator实例(SimpleRegistry通过直接反射)
    		 	 List<? extends Validator> validatorsFound = registry.findByType(validatorClass);
    	
    		 }
    	}
    	// 将validator转换为AnnotationValidator
    	AnnotationValidator av = new AnnotationValidator();
        av.setField(field);
        av.setMethod(ReflectionUtil.getGetterMethod(clazz, field));
        av.setValidators(validators);
        av.setGroups(groups);
        
    	annotationValidators.add(av);
    }
    

Spring AOP集成

  1. @FluentValid注解:表示需要Spring利用切面拦截方法,对参数利用FluentValidator做校验
@Service
public class CarServiceImpl implements CarService {
 
    @Override
    public Car addCar(@FluentValid Car car) {
        System.out.println("Come on! " + car);
        return car;
    }
}
  1. 定义FluentValidator拦截器:FluentValidateInterceptor
    2.1 FluentValidateInterceptor#invoke进行拦截
    2.2 配置FluentValidator
    @Bean
    public FluentValidateInterceptor fluentValidateInterceptor(){
        FluentValidateInterceptor interceptor = new FluentValidateInterceptor();
        interceptor.setCallback(new DefaultFluentCallBack());
        return interceptor;
    }
    
    @Bean
    public Advisor pointcutAdvisor(FluentValidateInterceptor fluentValidateInterceptor) {
        AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, SnailFluentValidate.class);
        // 配置增强类advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        advisor.setAdvice(fluentValidateInterceptor);
        return advisor;
    }	
    

源码浅析

  1. FluentValidator#doValidate
    1.1 按照默认验证回调条件,开始使用验证
     public FluentValidator doValidate() {
        return doValidate(defaultCb);
    }
    
  2. FluentValidator#doValidate
    public FluentValidator doValidate(ValidateCallback cb) {
    	//将分组设置到ThreadLocal中
    	GroupingHolder.setGrouping(groups);
    	// 遍历所有校验链进行校验(在 .on(car.getLicensePlate(), new CarLicensePlateValidator())设置)
    	for (ValidatorElement element : validatorElementList.getAllValidatorElements()) {
    		// 校验对象
    		Object target = element.getTarget();
    		// 对应的校验器
    		Validator v = element.getValidator();
    		try {
    			// 该校验器是否校验该对象
    			if (v.accept(context, target)) {
    				// 执行校验
    				if (!v.validate(context, target)) {
    						//校验失败后,结果设置false
    				 		result.setIsSuccess(false);
    				 		// 如果为快速失败,直接从校验链退出
                            if (isFailFast) {
                                break;
                            }
    				}
    			}
    		}catch (Exception e) {
    			// 校验器的异常回调(ValidatorHandler默认啥都不处理)
    			 v.onException(e, context, target);
    			 // 回调函数的异常回调(默认直接抛出异常)
                 cb.onUncaughtException(v, e, target);
    		}
    	}
    	if (result.isSuccess()) {
    		// 回调函数的成功回调
            cb.onSuccess(validatorElementList);
         } else {
         	// 回调函数的失败回调
            cb.onFail(validatorElementList, result.getErrors());
        }
    }
    

改造后的版本简述

  1. 配置校验拦截器
    @Bean
    public SnailFluentValidateInterceptor snailFluentValidateInterceptor(){
        SnailFluentValidateInterceptor interceptor = new SnailFluentValidateInterceptor();
        return interceptor;
    }
    @Bean
        public Advisor pointcutAdvisor(SnailFluentValidateInterceptor snailFluentValidateInterceptor) {
            AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, SnailFluentValidate.class);
            // 配置增强类advisor
            DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
            advisor.setPointcut(pointcut);
            advisor.setAdvice(snailFluentValidateInterceptor);
            return advisor;
        }
    
  2. SnailFluentValidateInterceptor是对FluentValidateInterceptor拦截器的增强
    2.1 只展示改动点
    public Object invoke(MethodInvocation invocation) throws Throwable {
    	   ValidatorContext context = new ValidatorContext();
           context.setResult(new ValidationResult());
           FluentValidator fluentValidator = FluentValidator.checkAll(groups)
                   .setExcludeGroups(excludeGroups)
                   .configure(registry)
                   .setIsFailFast(isFailFast)
                   .withContext(context);
           ...........
           if (result != null) {
                  LOGGER.debug(result.toString());
                   if(i+1<=arguments.length){
                       Object argument  = arguments[i+1];
                       if(argument instanceof SnailFluentResult){
                           arguments[i+1] = new SnailFluentResult(result,context);
                       }
                   }
       		}
    }
    
  3. 使用
    3.1 对象属性配置校验器
    public class OrderApiRequest {
    @NotBlank(message = "content不能为空")
    @FluentValidate({CheckContentValidator.class})
    private String content;
    
    @NotBlank(message = "token不能为空")
    @FluentValidate({CheckTokenValidator.class })
    private String token;
    
    @NotBlank(message = "sign不能为空")
    @FluentValidate({CheckSignValidator.class})
    private String sign;
    }
    
    3.2 需要拦截校验方法加上注解,并且接受返回
    @SnailFluentValidate
    public Map<String, Object> main(@FluentValid OrderApiRequest orderApiRequest, SnailFluentResult result){
    	if(!result.isSuccess()){
            backMap.put(WebCst.MSG,result.getErrorMsg());
            return ResponseUtl.response(backMap, StatusCst.sys1);
        }
        String service = (String) result.getAttribute("service");
        MethodCst methodCst = MethodCst.forName(service);
        if (Objects.isNull(methodCst)) {
            backMap.put(WebCst.MSG, "请求的服务未定义");
            return ResponseUtl.response(backMap, StatusCst.sys1);
        }
    }
    

参考资料

  1. Hibernate-Validator官网
  2. fluent-validator
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值