zoukankan      html  css  js  c++  java
  • Springboot集成BeanValidation扩展二:加载jar中的资源文件

    一、需求

    今天在搭建Springboot框架的时候,又遇到一个需求:在多模块系统中,有些模块想自己管理BeanValidation的资源文件(默认是启动项目claspath下的 ValidationMessages.properties)。刚开始还天真地认为springboot会不会帮我们做了,结果并没有,于是就是撸源码了。

    以下是我的实现和实现原理

    二、实现

    @Configuration
    public class MyWebMvcConfigurer implements WebMvcConfigurer {
    /**
     * 当有异常时返回默认的验证器
     * @return 返回的是org.springframework.validation.Validator,不是javax.validation.Validator
     * 所以返回时要适配一下
     */
    @Override
    public Validator getValidator() {
    	//路径匹配
    	PathMatchingResourcePatternResolver resourcePatternResolver =
    			new PathMatchingResourcePatternResolver(
            MyWebMvcConfigurer.class.getClassLoader());
    	try {
    		//匹配属性文件,有个限制,资源文件名称必须包含Validation
    		Resource[] resources = 
                resourcePatternResolver.getResources("classpath*:*Validation*.properties");
    
    		List<String> files = Arrays.stream(resources)
    				.filter(resource -> StringUtils.isNotBlank(resource.getFilename()))
    				.map(resource -> {
    					String fileName = resource.getFilename();
    					return fileName.substring(0, fileName.indexOf("."));
    				}).collect(Collectors.toList());
    
    		javax.validation.Validator validator = Validation.byDefaultProvider()
    				.configure()
                	 //这里可以加载多个文件
    				.messageInterpolator(new ResourceBundleMessageInterpolator(
                        new AggregateResourceBundleLocator(files)))
    				.buildValidatorFactory()
    				.getValidator();
            //适配
    		return new SpringValidatorAdapter(validator);
    	} catch (IOException e) {
           	//发生异常,返回null,springboot框架会采用默认的validator
    		return null;
    	}
    }
    

    三、实现原理

    源码分析

    1、定位Bean在什么地方验证的

    DispatcherServlet验证Bean的主要源码路径

    • RequestResponseBodyMethodProcessor#resolveArgument
      • AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
        • DataBinder#validate(核心)

    源码:

    /**
     * 该方法定位:org.springframework.web.servlet.mvc.method.annotation.
     			RequestResponseBodyMethodProcessor#resolveArgument
     			
     * Throws MethodArgumentNotValidException if validation fails.
     * @throws HttpMessageNotReadableException if {@link RequestBody#required()}
     * is {@code true} and there is no body content or if there is no suitable
     * converter to read the content with.
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) 
        throws Exception {
    
    	parameter = parameter.nestedIfOptional();
    	Object arg = readWithMessageConverters(webRequest, parameter, 
                                               parameter.getNestedGenericParameterType());
    	String name = Conventions.getVariableNameForParameter(parameter);
    
    	if (binderFactory != null) {
    		WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
    		if (arg != null) {
                //*******校验方法参数是否符合要求*******
                //调用链:也就是验证器被调用的地方
    			validateIfApplicable(binder, parameter);
    			if (binder.getBindingResult().hasErrors() 
                    && isBindExceptionRequired(binder, parameter)) {
    				throw 
                        new MethodArgumentNotValidException(parameter, 
                                                            binder.getBindingResult());
    			}
    		}
    		if (mavContainer != null) {
    			mavContainer.addAttribute(
                    BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
    		}
    	}
    
    	return adaptArgumentIfNecessary(arg, parameter);
    }
    
    /**
     * 该方法定位:org.springframework.web.servlet.mvc.method.annotation.
        AbstractMessageConverterMethodArgumentResolver#validateIfApplicable
     *  
     * Validate the binding target if applicable.
     * <p>The default implementation checks for {@code @javax.validation.Valid},
     * Spring's {@link org.springframework.validation.annotation.Validated},
     * and custom annotations whose name starts with "Valid".
     * @param binder the DataBinder to be used
     * @param parameter the method parameter descriptor
     * @since 4.1.5
     * @see #isBindExceptionRequired
     */
    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    	Annotation[] annotations = parameter.getParameterAnnotations();
    	for (Annotation ann : annotations) {
    		Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
    		if (validatedAnn != null || 
                ann.annotationType().getSimpleName().startsWith("Valid")) {
    			Object hints = (validatedAnn != null ? 
                                validatedAnn.value() : AnnotationUtils.getValue(ann));
    			Object[] validationHints = (hints instanceof Object[] ? 
                                            (Object[]) hints : new Object[] {hints});
                //调用链
    			binder.validate(validationHints);
    			break;
    		}
    	}
    }
    
    
    /**
     * 该方法定位:org.springframework.validation.DataBinder#validate(java.lang.Object...)
     *
     * Invoke the specified Validators, if any, with the given validation hints.
     * <p>Note: Validation hints may get ignored by the actual target Validator.
     * @param validationHints one or more hint objects to be passed to a {@link SmartValidator}
     * @see #setValidator(Validator)
     * @see SmartValidator#validate(Object, Errors, Object...)
     * 核心方法
     */
    public void validate(Object... validationHints) {
    	for (Validator validator : getValidators()) {
    		if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
    			((SmartValidator) validator).validate(getTarget(), 
                                                      getBindingResult(), validationHints);
    		}
    		else if (validator != null) {
    			validator.validate(getTarget(), getBindingResult());
    		}
    	}
    }
    /**
     * 获取验证器(关键就在:this.validators怎么初始化的?)
     */
    public List<Validator> getValidators() {
    	return Collections.unmodifiableList(this.validators);
    }
    

    发现:在DataBinder#validate中有验证Bean的核心代码validator.validate(...)

    分析到这里关键就是validator在哪赋值的?

    2、validators赋值

    • DataBinder属性validators赋值
      private final List validators = new ArrayList<>();

      //断点跟踪发现:
      public void setValidator(@Nullable Validator validator) {
          assertValidators(validator);
          this.validators.clear();
          if (validator != null) {
              this.validators.add(validator);
          }
      
      }
      
      • DataBinder#setValidator被调用的位置

        • org.springframework.web.bind.support.ConfigurableWebBindingInitializer#initBinder

        源码:

        @Override
        public void initBinder(WebDataBinder binder) {
        	binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
        	if (this.directFieldAccess) {
        		binder.initDirectFieldAccess();
        	}
        	if (this.messageCodesResolver != null) {
        		binder.setMessageCodesResolver(this.messageCodesResolver);
        	}
        	if (this.bindingErrorProcessor != null) {
        		binder.setBindingErrorProcessor(this.bindingErrorProcessor);
        	}
        	if (this.validator != null && binder.getTarget() != null &&
        			this.validator.supports(binder.getTarget().getClass())) {
        		//发现是在这里调用的,下面的问题就是ConfigurableWebBindingInitializer
        		//中的validator属性在哪初始化的?
        		//在对应的setValidator方法打断点
        		binder.setValidator(this.validator);
        	}
        	if (this.conversionService != null) {
        		binder.setConversionService(this.conversionService);
        	}
        	if (this.propertyEditorRegistrars != null) {
        		for (PropertyEditorRegistrar propertyEditorRegistrar : 		   
                     this.propertyEditorRegistrars) {
        			propertyEditorRegistrar.registerCustomEditors(binder);
        		}
        	}
        }
        
      • ConfigurableWebBindingInitializer#initBinder被调用的位置
        研究发现:ConfigurableWebBindingInitializer#initBinder是在springboot初始化时被调用的
        调用链如下:
        调用链1:初始化springmvc的requestMappingHandlerAdapter
        EnableWebMvcConfiguration#requestMappingHandlerAdapter

        • super.requestMappingHandlerAdapter();—>WebMvcConfigurationSupport
          调用链2:
          WebMvcConfigurationSupport#requestMappingHandlerAdapter
        • adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
          调用链3:
          EnableWebMvcConfiguration#ConfigurableWebBindingInitializer
        • super.getConfigurableWebBindingInitializer();
          调用链4:
          WebMvcConfigurationSupport#ConfigurableWebBindingInitializer
        • mvcValidator(),这个是核心

        源码:

        /**
         * @see org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.
         * EnableWebMvcConfiguration#requestMappingHandlerAdapter
         */
        @Bean
        @Override
        public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
            //调用链1:调用父类WebMvcConfigurationSupport#requestMappingHandlerAdapter
        	RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
        	adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null
        			|| this.mvcProperties.isIgnoreDefaultModelOnRedirect());
        	return adapter;
        }
        
        /**
         * @seeorg.springframework.web.servlet.config.annotation.
         * WebMvcConfigurationSupport#requestMappingHandlerAdapter
         *
         */
        public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        	RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
        	adapter.setContentNegotiationManager(mvcContentNegotiationManager());
        	adapter.setMessageConverters(getMessageConverters());
            //调用链2:EnableWebMvcConfiguration#getConfigurableWebBindingInitializer
        	adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
        	adapter.setCustomArgumentResolvers(getArgumentResolvers());
        	adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
        	...
        }
        
        /**
         * @see springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.
         * EnableWebMvcConfiguration#getConfigurableWebBindingInitializer
         *
         */
        @Override
        protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
        	try {
                //这里是不存在实例,报异常
        		return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
        	}
        	catch (NoSuchBeanDefinitionException ex) {
                //调用链3:WebMvcConfigurationSupport#getConfigurableWebBindingInitializer
        		return super.getConfigurableWebBindingInitializer();
        	}
        }
        
        /**
         * @seespringframework.web.servlet.config.annotation.WebMvcConfigurationSupport
         * #getConfigurableWebBindingInitializer
         *
         */
        protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
        	ConfigurableWebBindingInitializer initializer = 
        	new ConfigurableWebBindingInitializer();
        	initializer.setConversionService(mvcConversionService());
            //调用链4:核心方法mvcValidator()
        	initializer.setValidator(mvcValidator());
        	MessageCodesResolver messageCodesResolver = getMessageCodesResolver();
        	if (messageCodesResolver != null) {
        		initializer.setMessageCodesResolver(messageCodesResolver);
        	}
        	return initializer;
        }
        

    3、validator是什么

    通过源码分析,找到了关键点就是mvcValidator(),现在对其分析,找出其返回的validator到底是什么?

    断点调试时发现mvcValidator()进入了

    org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#intercept

    • org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#resolveBeanReference
      resolveBeanReference方法里有个关键的代码

      //关键在于 beanFactory.getBean(beanName),name = "mvcValidator",创建该实例
      //从而会找到EnableWebMvcConfiguration的mvcValidator方法
      //(因为mvcValidator方法上有@Bean,方法名称又与beanName相同,故调用)
      Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) :
                             beanFactory.getBean(beanName));
      
    • org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#mvcValidator
      终于找到创建validator对象的点了,以下就是如何自己扩展?
      继续研究创建validator的源码,寻找关键点

      @Bean
      @Override
      public Validator mvcValidator() {
      	if (!ClassUtils.isPresent("javax.validation.Validator",
                                    getClass().getClassLoader())) {
      		return super.mvcValidator();
      	}
          //关键在于getValidator()方法
          //真正调用的是父类DelegatingWebMvcConfiguration#getValidator
      	return ValidatorAdapter.get(getApplicationContext(), getValidator());
      }
      

    4、关键点:分析getValidator()方法

    注意:这里就是我们可以扩展的地方

    /**
     * springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#getValidator
     */
    @Override
    @Nullable
    protected Validator getValidator() {
    	//configurers属性是WebMvcConfigurerComposite的对象
    	return this.configurers.getValidator();
    }
    
    /**
     *@see springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#getValidator
     */
    @Override
    public Validator getValidator() {
    	Validator selected = null;
        //看到WebMvcConfigurer这个东西,我是很激动呀!终于看到曙光了,激动半天
        //于是我就自定义了MyWebMvcConfigurer实现WebMvcConfigurer,并重写
        //其中的getValidator方法,哈哈,终于找到扩展点了
    	for (WebMvcConfigurer configurer : this.delegates) {
    		Validator validator = configurer.getValidator();
    		if (validator != null) {
    			if (selected != null) {
    				throw new IllegalStateException("No unique Validator found: {" +
    						selected + ", " + validator + "}");
    			}
    			selected = validator;
    		}
    	}
    	return selected;
    }
    

    通过getValidator()获取自定义的validator后

    ValidatorAdapter.get(getApplicationContext(), getValidator());对其包装如下:

    /**
     * @see springframework.boot.autoconfigure.validation.ValidatorAdapter#get
     */
    public static Validator get(ApplicationContext applicationContext,
    		Validator validator) {
        //如果为null(自定义的validator发生异常),返回默认的
    	if (validator != null) {
            //因为非空,会执行该行代码
    		return wrap(validator, false);
    	}
    	return getExistingOrCreate(applicationContext);
    }
    
    private static Validator wrap(Validator validator, boolean existingBean) {
    	if (validator instanceof javax.validation.Validator) {
            //执行该代码
    		if (validator instanceof SpringValidatorAdapter) {
    			return new ValidatorAdapter((SpringValidatorAdapter) validator,
    					existingBean);
    		}
    		return new ValidatorAdapter(
    				new SpringValidatorAdapter((javax.validation.Validator) validator),
    				existingBean);
    	}
    	return validator;
    }
    

    总结:在分析源码的过程中犯了最大的错误就是:总想什么都搞明白,跟踪每个源码的实现,结果发现还是没搞懂,白白浪费了很多时间。其实在分析源码的过程中,不需要钻牛角尖,把每个都搞懂。你要搞明白你的“关注点“在哪?,不要走着走着就走偏了。很多源码“观其大意”就行,没必要深究,不然就呵呵了。

  • 相关阅读:
    TestNG中DataProvider的用法
    性能调优过程发现的问题
    20170221——接口自动化测试代码提交流程
    svn忽略target文件
    springboot搭建dubbo+zookeeper简单案例
    docker上启动mysql镜像,mysq中记录乱码解决方法
    docker在linux上的安装
    使用jave1.0.2将amr文件转成其他格式报错解决方案
    使用fio命令查看磁盘iops
    解决使用maven clean项目的时候报错,删除target文件夹失败
  • 原文地址:https://www.cnblogs.com/liruiloveparents/p/9400426.html
Copyright © 2011-2022 走看看