一、需求

今天在搭建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中的资源文件的更多相关文章

  1. SpringBoot集成Shiro 实现动态加载权限

    一.前言 本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,不用重启项目,以及在页面分配给用户 角色 . 按钮 .ur ...

  2. 从jar中读取资源文件的问题

    在项目中遇到了一个问题,在IDE中读取配置文件的程序,打成架包以后,放到服务器上跑,报找不到资源文件的错误. 其中,资源文件的路径如下: 获取配置文件路径和读取的方法如下: private stati ...

  3. Springboot集成BeanValidation扩展一:错误提示信息加公共模板

    Bean Validator扩展 1.需求 ​ 在使用validator时,有个需求就是公用错误提示信息,什么意思? 举个例子: ​ @NotEmpty非空判断,在资源文件中我不想每个非空判断都写”不 ...

  4. 从webview中加载assets中的html文件

    private void readHtmlFormAssets(){ WebSettings webSettings = tipsWebView.getSettings(); webSettings. ...

  5. Java动态加载jar及class文件

    经常碰到需要动态加载jar及class文件的场景.Java类由于需要加载和编译字节码,动态加载class文件较为麻烦,但JDK仍提供了一整套方法来动态加载jar文件和class文件. 一.动态加载ja ...

  6. Java_Java中动态加载jar文件和class文件

    转自:http://blog.csdn.net/mousebaby808/article/details/31788325 概述 诸如tomcat这样的服务器,在启动的时候会加载应用程序中lib目录下 ...

  7. [转载] Java中动态加载jar文件和class文件

    转载自http://blog.csdn.net/mousebaby808/article/details/31788325 概述 诸如tomcat这样的服务器,在启动的时候会加载应用程序中lib目录下 ...

  8. [Android] Android 用于异步加载 ContentProvider 中的内容的机制 -- Loader 机制 (LoaderManager + CursorLoader + LoaderManager.LoaderCallbacks)

    Android 用于异步加载 ContentProvider 中的内容的机制 -- Loader 机制 (LoaderManager + CursorLoader + LoaderManager.Lo ...

  9. java 从jar包中读取资源文件

    在代码中读取一些资源文件(比如图片,音乐,文本等等),在集成环境(Eclipse)中运行的时候没有问题.但当打包成一个可执行的jar包(将资源文件一并打包)以后,这些资源文件找不到,如下代码: Jav ...

随机推荐

  1. C++寒假学习计划

    课程 中国大学mooc西北工业大学c++程序设计 理由 本课程有48节,章节分类清晰,由许多小知识块组成,条例清晰便于学习,由基础开始,由浅入深,适合我这种小白. 计划 从2.8号至2.28除去2.1 ...

  2. android桌面悬浮窗实现

                            首先是一个小的悬浮窗显示的是当前使用了百分之多少的内存,点击一下小悬浮窗,就会弹出一个大的悬浮窗,可以一键加速.好,我们现在就来模拟实现一下类似的效果. ...

  3. vb如何将数据库中某个字段显示在一个文本框

    Dim mrc As ADODB.Recordset Private Sub cmdQuery_Click() Dim txtSQL As String Dim MsgText As String t ...

  4. RabbitMQ吞吐量测试-PerfTest上

    RabbitMQ吞吐量测试-PerfTest上 PerfTest RabbitMQ有一个基本的吞吐量测试工具PerfTest(文档,源代码和版本),它基于Java客户端,可以配置为模拟基本工作负载.P ...

  5. Scala 基础(5)—— 构建函数式对象

    有了 Scala 基础(4)—— 类和对象 的前提,现在就可以来构建一个基于 Scala 的函数式对象. 下面开始构造一个有理数对象 Rational. 1. 主构造方法和辅助构造方法 对于每一个类的 ...

  6. jquery使滚动条滚动到最底部

    $('body').scrollTop($('body')[0].scrollHeight); //想要加载页面自动到最底部要写入function中使用setTimeout function top1 ...

  7. 转:用VMProtect和ASProtect 的SDK加密应用程序

    最近想用VMProtect和ASProtect 的SDK加密一个程序,结果搞了半天没搞成,网上没看到在VC中如何使用VMProtect的SDK加密,于是琢磨了一下,总算成功了,最后有一点点心得,与大家 ...

  8. AGC019-E Shuffle and Swap

    给定两个长度为\(n\le 10^5\)的\(01\)串 \(A, B\), 满足 \(1\) 的数量相等 求通过下列方式将\(A\)变成\(B\)的概率 (mod意义下) 构造序列\(a,b\). ...

  9. linu触摸屏幕

    一..前提知识 1.Linux输入子系统(Input Subsystem): 在Linux中,输入子系统是由输入子系统设备驱动层.输入子系统核心层(Input Core)和输入子系统事件处理层(Eve ...

  10. 51nod 1851 俄罗斯方块

    玩过俄罗斯方块?那你知道俄罗斯方块共有七种吧(其实只有五种) 给一个黑白图,每次能将某些区域的格子黑白反转,至于某些区域的意思嘛,就是俄罗斯方块形状的区域咯(可水平翻转.上下翻转.旋转) 求能否将图变 ...