背景 笔者在最近应用国际化校验的时候,碰到一个奇怪的问题:国际化始终不能生效,消息返回的仍旧是模板消息。
相关源码 Java Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Data public  class  DemoParam      @NotNull (message = "{validator.demo.name.not-null}" )     private  String name;     @NotNull (         groups = DemoParamValidateGroup1.class,         message = "{validator.demo.title.not-blank}"      )     @NotEmpty (         groups = DemoParamValidateGroup1.class,         message = "{validator.demo.title.not-blank}"      )     @Length (         min = 1 ,         max = 64 ,         groups = DemoParamValidateGroup1.class,         message = "{validator.demo.title.illegal-length-1-64}"      )     private  String title;          public  interface  DemoParamValidateGroup1   } 
DemoApi:
1 2 3 4 @PostMapping ("/param1" )public  Object param1 (@Validated @RequestBody DemoParam demoParam)      return  Result.getSuccResult(demoParam); } 
ValidatorConfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration public  class  ValidatorConfig      private  final  MessageSource messageSource;     public  ValidatorConfig (MessageSource messageSource)           this .messageSource = messageSource;     }     @Bean      public  Validator validator ()           LocalValidatorFactoryBean validator = new  LocalValidatorFactoryBean();         validator.setValidationMessageSource(messageSource);         return  validator;     } } 
DemoApiTest:
1 2 3 4 5 6 7 8 9 10 11 @Test public  void  test_param1_default ()  throws  Exception     DemoParam demoParam = new  DemoParam();     mockMvc.perform(         post("/api/demo/param1" )             .contentType(MediaType.APPLICATION_JSON_UTF8)             .content(JSON.toJSONString(demoParam)))         .andExpect(status().isOk())         .andExpect(jsonPath("$.code" , is(DemoResultCode.BAD_REQUEST.getCode()))); } 
定位问题 由于笔者并没有在以前看过spring的validator源码;所以,打算从校验的执行入口处入手。
请求是POST形式,而在类RequestResponseBodyMethodProcessor的resolveArgument方法内,会对注解有@RequestBody的参数做参数解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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); } 
在上述的解析块内,① 处的代码是使用MessageHttpConverters将json字符串转化为目标实例;② 处的代码通过创建的WebDataBinder获取校验后的结果,通过结果判断是否校验通过。而我们需要的错误信息构建肯定在validateIfApplicable(binder, parameter);语句内。
1 2 3 4 5 6 7 8 9 10 11 12 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 ;     }   } } 
③ 处在校验参数的时候,会校验参数的注解是否有注解,如果注解为@Validated或者注解以Valid开头,则校验该参数,如 ④ 处的代码;binder是类DataBinder的实例,校验的逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  void  validate (Object... validationHints)    Object target = getTarget();   Assert.state(target != null , "No target to validate" );   BindingResult bindingResult = getBindingResult(); ⑤      for  (Validator validator : getValidators()) {     if  (!ObjectUtils.isEmpty(validationHints) && validator instanceof  SmartValidator) {       ((SmartValidator) validator).validate(target, bindingResult, validationHints); ⑥     }     else  if  (validator != null ) {       validator.validate(target, bindingResult); ⑥     }   } } 
⑤ 处代码创建一个默认的校验结果,然后传递进入实际的校验方法 ⑥ 内。在Spring Boot框架内,校验框架的实现交由Hibernate Validator实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  final  <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {  Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );   sanityCheckGroups( groups );   ValidationContext<T> validationContext = getValidationContextBuilder().forValidate( object );   if  ( !validationContext.getRootBeanMetaData().hasConstraints() ) {     return  Collections.emptySet();   }   ValidationOrder validationOrder = determineGroupValidationOrder( groups );   ValueContext<?, Object> valueContext = ValueContext.getLocalExecutionContext(       validatorScopedContext.getParameterNameProvider(),       object,       validationContext.getRootBeanMetaData(),       PathImpl.createRootPath()   );   return  validateInContext( validationContext, valueContext, validationOrder ); ⑦ } 
在 ⑦ 处,通过校验和值的上下文校验具体的内容;之后在ConstraintTree类内做具体的校验,其中的层次调用就不在本篇内描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 protected  final  <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext,			ValueContext<?, ?> valueContext, 			ConstraintValidatorContextImpl constraintValidatorContext, 			ConstraintValidator<A, V> validator) {   boolean  isValid;   try  {     @SuppressWarnings ("unchecked" )     V validatedValue = (V) valueContext.getCurrentValidatedValue();     isValid = validator.isValid( validatedValue, constraintValidatorContext );   }   catch  (RuntimeException e) {     if  ( e instanceof  ConstraintDeclarationException ) {       throw  e;     }     throw  LOG.getExceptionDuringIsValidCallException( e );   }   if  ( !isValid ) {               return  executionContext.createConstraintViolations( ⑧         valueContext, constraintValidatorContext     );   }   return  Collections.emptySet(); } 
在 ⑧ 处,可以看到在这里创建错误信息的实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public  ConstraintViolation<T> createConstraintViolation (ValueContext<?, ?> localContext, ConstraintViolationCreationContext constraintViolationCreationContext, ConstraintDescriptor<?> descriptor)    String messageTemplate = constraintViolationCreationContext.getMessage(); ⑨   String interpolatedMessage = interpolate( ⑩       messageTemplate,       localContext.getCurrentValidatedValue(),       descriptor,       constraintViolationCreationContext.getMessageParameters(),       constraintViolationCreationContext.getExpressionVariables()   );      Path path = PathImpl.createCopy( constraintViolationCreationContext.getPath() );   Object dynamicPayload = constraintViolationCreationContext.getDynamicPayload();   switch  ( validationOperation ) {     case  PARAMETER_VALIDATION:       return  ConstraintViolationImpl.forParameterValidation(           messageTemplate,           constraintViolationCreationContext.getMessageParameters(),           constraintViolationCreationContext.getExpressionVariables(),           interpolatedMessage,           getRootBeanClass(),           getRootBean(),           localContext.getCurrentBean(),           localContext.getCurrentValidatedValue(),           path,           descriptor,           localContext.getElementType(),           executableParameters,           dynamicPayload       );     case  RETURN_VALUE_VALIDATION:       return  ConstraintViolationImpl.forReturnValueValidation(           messageTemplate,           constraintViolationCreationContext.getMessageParameters(),           constraintViolationCreationContext.getExpressionVariables(),           interpolatedMessage,           getRootBeanClass(),           getRootBean(),           localContext.getCurrentBean(),           localContext.getCurrentValidatedValue(),           path,           descriptor,           localContext.getElementType(),           executableReturnValue,           dynamicPayload       );     default :       return  ConstraintViolationImpl.forBeanValidation(           messageTemplate,           constraintViolationCreationContext.getMessageParameters(),           constraintViolationCreationContext.getExpressionVariables(),           interpolatedMessage,           getRootBeanClass(),           getRootBean(),           localContext.getCurrentBean(),           localContext.getCurrentValidatedValue(),           path,           descriptor,           localContext.getElementType(),           dynamicPayload       );   } } 
在 ⑨ 处,获取原消息的消息模板,即:{validator.demo.name.not-null},之后通过interpolate方法,将模板消息替换为解析后的字符串。
一层层递归:ValidationContext::interpolate -> AbstractMessageInterpolator::interpolate -> AbstractMessageInterpolator::interpolateMessage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 private  String interpolateMessage (String message, Context context, Locale locale)  throws  MessageDescriptorFormatException         if  ( message.indexOf( '{'  ) < 0  ) {     return  replaceEscapedLiterals( message );   }   String resolvedMessage = null ;         if  ( cachingEnabled ) {     resolvedMessage = resolvedMessages.computeIfAbsent( new  LocalizedMessage( message, locale ), lm -> resolveMessage( message, locale ) );   }   else  {     resolvedMessage = resolveMessage( message, locale );    }      if  ( resolvedMessage.indexOf( '{'  ) > -1  ) {          resolvedMessage = interpolateExpression(         new  TokenIterator( getParameterTokens( resolvedMessage, tokenizedParameterMessages, InterpolationTermType.PARAMETER ) ),         context,         locale     );          resolvedMessage = interpolateExpression(         new  TokenIterator( getParameterTokens( resolvedMessage, tokenizedELMessages, InterpolationTermType.EL ) ),         context,         locale     );   }      resolvedMessage = replaceEscapedLiterals( resolvedMessage );   return  resolvedMessage; } 
通过 resolveMessage( message, locale ) 方法,会真正将消息转化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private  String resolveMessage (String message, Locale locale)    String resolvedMessage = message;   ResourceBundle userResourceBundle = userResourceBundleLocator       .getResourceBundle( locale );   ResourceBundle constraintContributorResourceBundle = contributorResourceBundleLocator       .getResourceBundle( locale );   ResourceBundle defaultResourceBundle = defaultResourceBundleLocator       .getResourceBundle( locale );   String userBundleResolvedMessage;   boolean  evaluatedDefaultBundleOnce = false ;   do  {          userBundleResolvedMessage = interpolateBundleMessage(         resolvedMessage, userResourceBundle, locale, true      );          if  ( !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) {       userBundleResolvedMessage = interpolateBundleMessage(           resolvedMessage, constraintContributorResourceBundle, locale, true        );     }               if  ( evaluatedDefaultBundleOnce && !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) {       break ;     }          resolvedMessage = interpolateBundleMessage(         userBundleResolvedMessage,         defaultResourceBundle,         locale,         false      );     evaluatedDefaultBundleOnce = true ;   } while  ( true  );   return  resolvedMessage; } 
在 ResourceBundle userResourceBundle = userResourceBundleLocator.getResourceBundle( locale ); 获取过程中,并没有获取到messages的bundle,也就是说,上文设置validator.setValidationMessageSource(messageSource);并没有生效。
解析问题 上文,笔者通过一步步定位了解到:validator设置的messageSource并没有生效。那么接下来,就需要探查下这个失效的原因。
ValidatorConfig内的Validator未执行?在笔者自定义的Validator注入Bean的方法内增加一个断点。然后重新启动应用,应用初始化过程顺利在断点处停留。那么,未执行的判断可以pass。
LocalValidatorFactoryBean的初始化过程未成功设置国际化?1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public  void  afterPropertiesSet ()    Configuration<?> configuration;   if  (this .providerClass != null ) {     ProviderSpecificBootstrap bootstrap = Validation.byProvider(this .providerClass);     if  (this .validationProviderResolver != null ) {       bootstrap = bootstrap.providerResolver(this .validationProviderResolver);     }     configuration = bootstrap.configure();   }   else  {     GenericBootstrap bootstrap = Validation.byDefaultProvider();     if  (this .validationProviderResolver != null ) {       bootstrap = bootstrap.providerResolver(this .validationProviderResolver);     }     configuration = bootstrap.configure();   }      if  (this .applicationContext != null ) {     try  {       Method eclMethod = configuration.getClass().getMethod("externalClassLoader" , ClassLoader.class);       ReflectionUtils.invokeMethod(eclMethod, configuration, this .applicationContext.getClassLoader());     }     catch  (NoSuchMethodException ex) {            }   }   MessageInterpolator targetInterpolator = this .messageInterpolator; ①   if  (targetInterpolator == null ) {     targetInterpolator = configuration.getDefaultMessageInterpolator();   }   configuration.messageInterpolator(new  LocaleContextMessageInterpolator(targetInterpolator)); ②   if  (this .traversableResolver != null ) {     configuration.traversableResolver(this .traversableResolver);   }   ConstraintValidatorFactory targetConstraintValidatorFactory = this .constraintValidatorFactory;   if  (targetConstraintValidatorFactory == null  && this .applicationContext != null ) {     targetConstraintValidatorFactory =         new  SpringConstraintValidatorFactory(this .applicationContext.getAutowireCapableBeanFactory());   }   if  (targetConstraintValidatorFactory != null ) {     configuration.constraintValidatorFactory(targetConstraintValidatorFactory);   }   if  (this .parameterNameDiscoverer != null ) {     configureParameterNameProvider(this .parameterNameDiscoverer, configuration);   }   if  (this .mappingLocations != null ) {     for  (Resource location : this .mappingLocations) {       try  {         configuration.addMapping(location.getInputStream());       }       catch  (IOException ex) {         throw  new  IllegalStateException("Cannot read mapping resource: "  + location);       }     }   }   this .validationPropertyMap.forEach(configuration::addProperty);      postProcessConfiguration(configuration);   this .validatorFactory = configuration.buildValidatorFactory(); ③   setTargetValidator(this .validatorFactory.getValidator()); } 
解释下国际化消息如何设置到validator工厂的逻辑:targetInterpolator 变量;而这个变量最终传递给了configuration,如 ③ 处。最后,在 ③ 处使用configuration的buildValidatorFactory方法构建validator的工厂。
笔者在validator的工厂类LocalValidatorFactoryBean初始化hook内设置了断点:然后启动应用,应用在执行了Validator的注入后,成功执行了LocalValidatorFactoryBean的初始化方法afterPropertiesSet;但是笔者在这里发现,这个初始化执行了两次。恰恰,通过this.messageInterpolator这个变量,笔者在第一次初始化的时候查看到用户定义的messageResource已经加载,如下图:
图片上的第一个红框是已成功加载的messagesource;而第二个红框是未加载的形式;在第二次初始化的时候,笔者在userResourceBundle未看到笔者定义的messagesource值,跟第二个红框即未加载的形式是一样的。
很好,成功定位到具体的问题:DataBinder使用的validator实例并不是笔者定义的实例,这也就是为什么国际化始终无法生效的原因。
解决问题 定位到问题所在,就该思考如何去解决这个问题。
按理来说,Spring Boot在用户自定义Validator后,会覆盖它自身的校验器,实际情况按照笔者定位的问题,这种覆盖情况并没有发生。
在这里提一句,Spring Boot集成校验器或者其他一些框架等等都是通过Configuration机制来实现(这个可以看笔者之前写的一篇文章:Spring-Bean解析分析过程 )。来找找Validator的自动化配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration @ConditionalOnClass (ExecutableValidator.class)@ConditionalOnResource (resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider" )@Import (PrimaryDefaultValidatorPostProcessor.class)public  class  ValidationAutoConfiguration  	@Bean  	@Role (BeanDefinition.ROLE_INFRASTRUCTURE) 	@ConditionalOnMissingBean (Validator.class) 	public  static  LocalValidatorFactoryBean defaultValidator ()   		LocalValidatorFactoryBean factoryBean = new  LocalValidatorFactoryBean(); 		MessageInterpolatorFactory interpolatorFactory = new  MessageInterpolatorFactory(); 		factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); 		return  factoryBean; 	} 	@Bean  	@ConditionalOnMissingBean  	public  static  MethodValidationPostProcessor methodValidationPostProcessor (  			Environment environment, @Lazy Validator validator)  		MethodValidationPostProcessor processor = new  MethodValidationPostProcessor(); 		boolean  proxyTargetClass = environment 				.getProperty("spring.aop.proxy-target-class" , Boolean.class, true ); 		processor.setProxyTargetClass(proxyTargetClass); 		processor.setValidator(validator); 		return  processor; 	} } 
可以在 ① 处看到,这个就是Spring Boot自身默认的校验器的一个初始化注入方法。并且,可以看到,在这里没有注入messageSource。
而这个方法上有标识@ConditionalOnMissingBean(Validator.class)注解,也就是说,如果已经存在Validator类,那么久不会执行Spring Boot自身校验器的初始化流程;这个就奇怪了,之前笔者自定义的Validator在注入后,并没有使得这个初始化失效。笔者尝试在这个方法上加了断点,启动应用后,笔者定义的Validator和Spring Boot自身的Validator都执行了初始化过程。
这个时候,笔者的内心真的是崩溃的,难不成Spring Boot的Conditional机制失效了???
突然想到,ConditionalOnMissingBean是根据类来判断的,那么会不会存在两个Validator类?然后对比了一下,发现了一个巨坑无比的事情:
笔者引入的全限定名:org.springframework.validation.ValidatorSpring Boot支持的全限定名:javax.validation.Validator
难怪一致无法成功覆盖默认配置。
而为什么类全限定名不一样,而仍旧可以返回LocalValidatorFactoryBean类的实例呢?因为,LocalValidatorFactoryBean类的父类SpringValidatorAdapter实现了javax.validation.Validator接口以及SmartValidator接口;而SmartValidator接口继承了org.springframework.validation.Validator接口。所以,对LocalValidatorFactoryBean类的实例来说,都可以兼容。
这个也就是为什么笔者在执行校验的时候,校验器直接返回消息模板而不是解析后的消息的原因所在。
总结 一句话,引入类的时候,以后还是要仔细点。