背景 笔者在最近应用国际化校验的时候,碰到一个奇怪的问题:国际化始终不能生效,消息返回的仍旧是模板消息。
相关源码 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.Validator
而Spring Boot
支持的全限定名:javax.validation.Validator
难怪一致无法成功覆盖默认配置。
而为什么类全限定名不一样,而仍旧可以返回LocalValidatorFactoryBean
类的实例呢?因为,LocalValidatorFactoryBean
类的父类SpringValidatorAdapter
实现了javax.validation.Validator
接口以及SmartValidator
接口;而SmartValidator
接口继承了org.springframework.validation.Validator
接口。所以,对LocalValidatorFactoryBean
类的实例来说,都可以兼容。
这个也就是为什么笔者在执行校验的时候,校验器直接返回消息模板而不是解析后的消息的原因所在。
总结 一句话,引入类的时候,以后还是要仔细点。