目录
1.源码解读-validator包
2. 源码解读-MessageSource
3.Spring-MessageSource与validator结合
4.实践
5.踩坑记录
前提:项目基于spring boot。
项目中,我们经常处理国际化消息及在校验中也需要抛出国际化消息,但是messages和validator的消息配置文件不一致,且处理器看起来也是不一样的,那么接下来以两者均需要国际化来看一下他们的处理的不同。
首先看validator,其中有一个处理消息的概念:interpolator,即“篡改”,也就是对于消息可进行最终输出前的处理,如占位符替换等,否则将直接输出配置的内容。接着,该概念下,需要具体的配置加载:org.hibernate.validator.spi.resourceloading.ResourceBundleLocator,作用就是根据国际化环境(en或zh等)加载相应的资源加载器,具体有以下几个:
org.hibernate.validator.resourceloading.AggregateResourceBundleLocator
org.hibernate.validator.resourceloading.CachingResourceBundleLocator
org.hibernate.validator.resourceloading.DelegatingResourceBundleLocator
org.hibernate.validator.resourceloading.PlatformResourceBundleLocator
org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator
以上几个类(非加粗)实现是基于装饰者模式的,基础的是 PlatformResourceBundleLocator,原理通过name去classpath路径加载对应环境的资源文件,然后从内容里获取key的值,另外几个从名字上也可以看出加强了什么:Aggregate聚合,cache缓存,delegate代理。
可以看到,其实最终起作用的是:java.util.ResourceBundle,即本地资源文件。
在看加粗类,它是基于messages中MessageSource的,接下来会讲到。
然后我们来看专门处理国际化消息的org.springframework.context.MessageSource,它下边的实现有很多,基本是各种不同来源,有基于本地内存的(map)、java.util.ResourceBundle等,与上面相同的就是基于java.util.ResourceBundle的org.springframework.context.support.ResourceBundleMessageSource,这也是我们经常使用的。
so,到此,两者可以联系起来了,但是,两者的默认资源名称是不同的。validator是ValidationMessages,message是messages。
所以,messages可以是validator的某个实现,也就是他俩是有交集的,当且仅当均使用同一个MessageSource,validator使用MessageSourceResourceBundleLocator时,可以通过messageSource进行统一控制。
好了,那这样的话,我们统一也就很简单了。
看了这么多,配置起来其实很简单:
@Configuration
public class ValidatorConfig {
@Autowired
private MessageSource messageSource;
@Bean
public Validator validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
factoryBean.setValidationMessageSource(messageSource);
return factoryBean;
}
}
本例中,messageSource使用的是默认的,即message为basename,只要在文件中放几个key,即可测试,现提供出来
控制器
@ResponseBody
@PostMapping("testValid")
public String testValid(@Valid User user){
System.out.println(user.getName());
return "success";
}
实体类
@Data
public class User {
@NotNull(message = "name不能为空")
private String name;
private String number;
@Min(value = 100, message = "{age}{validation.message.min}")
private int age;
}
马上去试试吧!
上述改动后,基本已将项目中Spring框架下的国际化和validator功能统一,一般的使用基本没问题,使用默认的messageSource也没问题,问题出在哪儿呢,当我们自定义开发messageSource时,一些细节需要注意,否则会导致很大的问题。
问题描述
好,带着我们的假设,进入源码:
校验功能会进入以下方法进行参数的解析(解析大括号的参数)与读取(messageSource)
org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator#interpolateBundleMessage
源码如下:
private String interpolateBundleMessage(String message, ResourceBundle bundle, Locale locale, boolean recursive)
throws MessageDescriptorFormatException {
TokenCollector tokenCollector = new TokenCollector( message, InterpolationTermType.PARAMETER );
TokenIterator tokenIterator = new TokenIterator( tokenCollector.getTokenList() );
while ( tokenIterator.hasMoreInterpolationTerms() ) {
String term = tokenIterator.nextInterpolationTerm();
String resolvedParameterValue = resolveParameter(
term, bundle, locale, recursive
);
tokenIterator.replaceCurrentInterpolationTerm( resolvedParameterValue );
}
return tokenIterator.getInterpolatedMessage();
}
重点关注resolveParameter方法,我们看看他做了什么
private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, boolean recursive)
throws MessageDescriptorFormatException {
String parameterValue;
try {
if ( bundle != null ) {
parameterValue = bundle.getString( removeCurlyBraces( parameterName ) );
if ( recursive ) {
parameterValue = interpolateBundleMessage( parameterValue, bundle, locale, recursive );
}
}
else {
parameterValue = parameterName;
}
}
catch (MissingResourceException e) {
// return parameter itself
parameterValue = parameterName;
}
return parameterValue;
}
看没看到,有一个异常拦截,也就是获取不到资源时,他会捕获该异常,并且把没有拿到源数据的参数重新赋值回原样,供之后拿配置的信息赋值使用。
nice!
补坑尝试:
按上面的方式,我在自定义的messageSource中没有获取到值时,抛出MissingResourceException,完美解决!
附上语言环境配置:
Java的系统启动后,通过Locale.getDefault()能够得到一个当前应用默认的Locale信息,但如果希望我们的应用不管部署在任意机器上,可以保持同一个默认Locale怎么办呢?也就是如何修改Java启动的默认Locale。
1. 可以在启动的入口代码处增加以下语句
Locale.setDefault(newLocale("en","US"));
2. 在Java启动时增加以下参数:-Duser.language=en -Duser.country=US
3. 修改操作系统的语言设置。
Windows: 控制面板 --> 地区语言
Linux: 永久方案 【vi /etc/sysconfig/i18n 修改如下LANG="en_US.UTF-8" 】
临时方案 【export LANG=en_US.UTF-8】