简介:
国际化信息也称为本地化信息,i18n是“国际化”的简称。在资讯领域,国际化(i18n)指让产品无需做大的改变就能够适应不同的语言和地区的需要。对程序来说,在不修改内部代码的情况下,能根据不同语言及地区显示相应的界面。
demo
step 1. 新增国际化资源问题
分别在三个文件中添加内容如下:
message.properties:表示默认的,里面可以没有值,但必须有这样的一个文件,可以参见源码:MessageSourceAutoConfiguration.ResourceBundleCondition#getMatchOutcomeForBasename
zh_CH
10001=你好,世界
10002=你好 JAVA
en_US
10001=hello word
10002=hello JAVA
step 2. 配置资源文件位置
spring.messages.basename=i18n.message
step 3. 配置解析器
SessionLocaleResolver为spring内置解析器之一,主要处理session会话级语言解析,其他还有CookieLocaleResolver
、FixedLocaleResolver
、AcceptHeaderLocaleResolver
@Configuration
public class LocaleConfig {
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.CHINA);//默认语言
return localeResolver;
}
}
step 4 配置拦截器
有了解析器,还需要拦截器来对请求的语言参数进行获取,采用默认的LocaleChangeInterceptor作为拦截器来指定切换国际化语言的参数名。比如当请求的url中包含?lang=zh_CN表示读取国际化文件messages_zh_CN.properties。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleInterceptor localeInterceptor = new LocaleInterceptor();
registry.addInterceptor(localeInterceptor);
}
}
@Slf4j
public class LocaleInterceptor extends LocaleChangeInterceptor {
private static final String LOCALE = "X-Locale";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String newLocale = request.getHeader(LOCALE);
if (newLocale != null) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
} catch (IllegalArgumentException e) {
if (isIgnoreInvalidLocale()) {
log.warn("Ignoring invalid locale value [{}]: ", newLocale, e);
} else {
throw e;
}
}
}
return true;
}
}
step 5 . i18n工具类
@Slf4j
@Component
public class LocaleMessageUtils {
@Autowired
private MessageSource messageSource;
public String getMessage(String code, Object[] args, String defaultMessage) {
try {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(code, args, defaultMessage, locale);
} catch (Exception e) {
log.error("get locale message failed, ErrorMsg:" + e.getMessage());
return defaultMessage;
}
}
}
step 6. 测试
源码分析
MessageSourceAutoConfiguration
ResourceBundleCondition作为是否加载该配置类的条件
//该方法主要根据配置资源路径是否能找到对应的资源,只要能找到资源时才能加载此配置类
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = cache.get(basename);
if (outcome == null) {
outcome = getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
springboot提供了国际化信息自动配置类,配置类中注册了ResourceBundleMessageSource实现类。
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
//设置资源路径
if (StringUtils.hasText(properties.getBasename())) {
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
//设置资源文件默认编码
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
//在找不到当前系统对应的资源文件时,如果该属性为 true,则会默认查找当前系统对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到系统默认的 messages.properties 文件
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
//设置缓存过期时间
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
//该参数控制的是,当输入参数为空时,是否还是使用MessageFormat.format函数对结果进行格式化,默认是 false;
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
//解析不到资源时是否用code作为返回值
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
RequestContextFilter
springmvc自动装配配置类,注册了一个RequestContextFilter过滤器,一次请求,LocaleContextHolder都会保存当前请求的本地化信息,一般用于设置默认本地化信息。
//处理过滤器
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
initContextHolders(request, attributes);
try {
filterChain.doFilter(request, response);
}
finally {
//请求结束清空本次请求本地化信息
resetContextHolders();
if (logger.isTraceEnabled()) {
logger.trace("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
}
//初始化本次请求的本地化配置
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
if (logger.isTraceEnabled()) {
logger.trace("Bound request context to thread: " + request);
}
}
//重置清空吧本地化配置
private void resetContextHolders() {
LocaleContextHolder.resetLocaleContext();
RequestContextHolder.resetRequestAttributes();
}
buildLocaleContext
DispatcherServlet#LocaleContextHolder
在每次请求都会用注册的LocaleResolver构造LocaleContext实例,如SessionLocaleResolver#resolveLocaleContext。如果不是LocaleContextResolver的话就直接取HttpServletRequest中的Locale返回,然后this.initContextHolders方法将解析后的Locale对象设值到LocaleContextHolder中:
@Override
protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
LocaleResolver lr = this.localeResolver;
if (lr instanceof LocaleContextResolver) {
return ((LocaleContextResolver) lr).resolveLocaleContext(request);
}
else {
return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale());
}
}
LocaleContextHolder是用来处理Local的上下文容器(其实就是内部维护了一个ThreadLocal),其中LocaleContext是一个用来获取Locale的接口
//设置当前请求上下文本地化信息
public static void setLocaleContext(@Nullable LocaleContext localeContext, boolean inheritable) {
if (localeContext == null) {
resetLocaleContext();
}
else {
if (inheritable) {
inheritableLocaleContextHolder.set(localeContext);
localeContextHolder.remove();
}
else {
localeContextHolder.set(localeContext);
inheritableLocaleContextHolder.remove();
}
}
}
ResourceBundleMessageSource
首先遍历各个basename, 如果资源文件没加载首先会根据basename、locale去加载资源,并将加载的内容缓存起来,然后从加载的资源文件中根据code查找对应的本地化message。该实接口实现类还有一个与之相近的版本ReloadableResourceBundleMessageSource,支持内容的过期个刷新。
@Override
protected String resolveCodeWithoutArguments(String code, Locale locale) {
Set<String> basenames = getBasenameSet();
for (String basename : basenames) {
ResourceBundle bundle = getResourceBundle(basename, locale);
if (bundle != null) {
String result = getStringOrNull(bundle, code);
if (result != null) {
return result;
}
}
}
return null;
}
MessageSourceControl
负责资源的具体加载逻辑
@Nullable
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
throws IllegalAccessException, InstantiationException, IOException {
//资源格式:properties
if (format.equals("java.properties")) {
//拼接资源名
String bundleName = toBundleName(baseName, locale);
final String resourceName = toResourceName(bundleName, "properties");
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream inputStream;
try {
//加载资源文件流
inputStream = AccessController.doPrivileged((PrivilegedExceptionAction<InputStream>) () -> {
InputStream is = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
connection.setUseCaches(false);
is = connection.getInputStream();
}
}
}
else {
is = classLoader.getResourceAsStream(resourceName);
}
return is;
});
}
catch (PrivilegedActionException ex) {
throw (IOException) ex.getException();
}
if (inputStream != null) {
String encoding = getDefaultEncoding();
//设置文件编码
if (encoding != null) {
try (InputStreamReader bundleReader = new InputStreamReader(inputStream, encoding)) {
//用Properties在家文件流,在资源转换成key-value形式
return loadBundle(bundleReader);
}
}
else {
try (InputStream bundleStream = inputStream) {
//用Properties在家文件流,在资源转换成key-value形式
return loadBundle(bundleStream);
}
}
}
else {
return null;
}
}
else {
//文件格式是class
return super.newBundle(baseName, locale, format, loader, reload);
}
}