一、需求

今天在搭建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. SPOJ 149 FSHEEP Fencing in the Sheep ( 计算几何 + 二分 )

    以下摘自SPOJ泛做表格: 题意:给定一个星形多边形,而且给出了一个可以看到形内所有点的位置(我们称这个点为观察点),让你判断有多少个点位于多边形内. 时间复杂度:O(mlogn) 将多边形上的点按极 ...

  2. Pandas根据条件赋值

    我们有以下判断条件,我们想要更改B中的数, 而更改的位置是取决于 A 的. 对于A大于4的位置. 更改B在相应位置上的数为0. df.B[df.A>4] = 0

  3. 利用反射修改final数据域

    当final修饰一个数据域时,意义是声明该数据域是最终的,不可修改的.常见的使用场景就是eclipse自动生成的serialVersionUID一般都是final的. 另外还可以构造线程安全(thre ...

  4. BeanUtils Date

    在jdbc封装(基础的CRUD)的时候(查询一条数据,查询多条数据,更新....)经常会用到一个BeanUtil来设置属性值,当对象中存在Date类型的时候,会报错:如下: 2017-11-03 13 ...

  5. Asymptotic I Catalan Number

    卡特兰数出现在许多计数问题中. 常见的例子有:$n$ 个节点的有序二叉树,$2n$ 个括号构成的合法括号序列. 在上面所举的两个例子中,很容易看出卡特兰数满足递推: $$ C_{n+1} = \sum ...

  6. eclipse快捷键及各种设置

    1.提示键配置一般默认情况下,Eclipse ,MyEclipse 的代码提示功能是比Microsoft Visual Studio的差很多的,主要是Eclipse ,MyEclipse本身有很多选项 ...

  7. bigdecimal的使用

    BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度 (scale) 组成.如果为零或正数,则标度是小数点后的位数.如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂. ...

  8. bootstrap3基本了解

    使用 BootCDN 提供的免费 CDN 加速服务(同时支持 http 和 https 协议) Bootstrap 中文网 为 Bootstrap 专门构建了免费的 CDN 加速服务,访问速度更快.加 ...

  9. 行为型设计模式之观察者模式(Observer)

    结构 意图 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新. 适用性 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面.将这二者封装在独 ...

  10. mysql 连接失败问题汇集

    FHost '192.168.5.128' is not allowed to connect to this MySQL serverConnection closed by foreign hos ...