Springboot集成BeanValidation扩展二:加载jar中的资源文件

摘要:
一开始,我天真地认为springboot会帮助我们,但没有成功,所以我只是剥离了源代码。这里是我的实现和实现原则II Implement@ConfigurationpublicclassMyWebMvcConfigureprimeElementsWebMvcConfigurator{/**在出现异常时返回默认验证器*@return返回org.springframework.validation.validation.Validator而不是javax.validation.Validator*,因此在返回时,需要调整*/@OverridepublicValidatorgetValidator(){//路径匹配PathMatchingResourcePatternResolversourcePatternResolver=newPathMatchingResource PatternResolve;请尝试{//匹配属性文件有限制。资源文件名必须包含ValidationResource[]resources=resourcePatternResolve.getResources;Listfiles=Arrays.stream.filter.map.complete;javax.validation.ValidatorValidator=validation.byDefaultProvider(). configure()//此处可以加载多个文件。消息插值器。buildValidatorFactory()。getValidator()//Adapt returnnewSpringValidatorAdapter;}Catch{//发生异常并返回null。spring-boot框架将使用默认的validatorreturnnull;}}III。实现原理源代码分析1确定DispatcherServlet验证bean RequestResponseBodyMethodProcessor#resolveArgumentAbstractMessageConverterMethodArgumentResolver#validateIfApplicableDataBinder#validate(core)的主源路径的位置源代码:/***方法位置:org.springframework。网状物servlet。mvc模式。方法通知。RequestResponseBodyMethodProcessor#resolveArgument*ThrowsMethodArgumentNotValidExceptionifvalidationfailures。*@throwsHttpMessageNotReadableException如果{@linkRequestBody#required()}*是{@codetrue},并且没有任何内容,或者如果没有可用的*转换器来读取内容。*/@OverridepublicObjectresolveArgumenthrowsException{parameter=parameter.nestedIfOption();Objectarg=readWithMessageConverters;Stringname=Conventions.getVariableNameForParameter;if(binderFactory!

一、需求

今天在搭建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;
}

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

免责声明:文章转载自《Springboot集成BeanValidation扩展二:加载jar中的资源文件》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇使用多线程时,传递 request 对象丢失微信支付报错:统一下单和拉起支付的appid不一致(原创)下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

chromium浏览器开发系列第三篇:chromium源码目录结构

上两篇介绍了下载源码和编译源码,这次主要介绍chromium的源码目录结构,我也是通过源码和官网结合来跟大家说,如果有说的不准确的,欢迎交流。 另外,官网的不一定准确,他们其实也很懒,所以最主要还是靠自己。官网只能作为一个参考。 Chromium结构相对两年前变化很大。目录结构依然很清晰,主要有三个部分(不包括其他的库):浏览器,渲染器,webkit。浏览...

ubuntu下下载并安装H265(hm.x.x代码和X265代码)

H265,现今是High Efficiency Video Coding的别称,详细的概述见维基百科,详细的开发见官方网站。 一、下载并编译官方的测试源码HM.x.x: 1 ubuntu下安装svn: apt-get install subversion 2打开官方的存放目录,(当然该界面也可以通过上面的官方主页进入)找到需要的分支或下载版本,并复制该网址...

linux中patch命令 -p 选项

 patch命令和diff命令是linux打补丁的成对命令,diff 负责生产xxxxx.patch文件,patch命令负责将补丁打到要修改的源码上。但是patch命令的参数-p很容易使人迷惑,因为对-p 后面的数字理解不清晰,造成patch打不上,项目时间拖延,很是郁闷。后来仔细实践了一下,弄清楚了-p实际的含义。        举例说明更加容易看懂。比...

即时聊天IM之二 openfire 整合现有系统用户

合肥程序员群:49313181。    合肥实名程序员群:128131462 (不愿透露姓名和信息者勿加入) Q  Q:408365330     E-Mail:egojit@qq.com  综述: 每天利用中午时间更新下这个知识点的的博客如果感兴趣的觉得更新慢了也别介意(其它时间还是以工作为主,学习工作两不误,哈哈……)。上一篇我纯理论上简单讲解了一下...

经典的CSS代码(转)

Web开发技术每年都在革新,浏览器已逐渐支持CSS3特性,并且网站设计师和前端开发者普遍采用这种新技术进行设计与开发。但仍然有一些开发者迷恋着一些CSS2代码。 分享20段非常专业的CSS2/CSS3代码供大家使用,你可以把它们保存在IDE里、或者存储在CSS文档里,这些代码片段绝对会给你带来意外的惊喜。 1. CSS Resets 网络上关于CSS重置的...

给ubuntu换内核

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 下载内核源码   有两种方式,一种方式是直接从官网:https://www.kernel.org/直接下载,另一种方式是通过git进行下载。   首先,在官网内核版本分为三种: mainline:主线版本,最新的 stable:稳定版本 longterm:长期支...