文章目录
前言
Spring MessageSource
与Java Locale
息息相关, 因此我们有必要先了解Locale
, 可以参考使用Java Locale进行国际化
官方文档
ApplicationContext
接口扩展了一个名为 MessageSource
的接口,因此提供了国际化(i18n
)功能。 Spring
还提供了 HierarchicalMessageSource
接口,可以分层解析消息。这些接口一起提供了 Spring
影响消息解析的基础。在这些接口上定义的方法包括:
-
String getMessage(String code, Object[] args, String default, Locale loc)
:用于从MessageSource
检索消息的基本方法。如果没有找到指定语言环境的消息,则使用默认消息。使用标准库提供的MessageFormat
功能,传入的任何参数都成为替换值。 -
String getMessage(String code, Object[] args, Locale loc)
:与前面的方法基本相同,但有一个区别:不能指定默认消息。如果找不到消息,则抛出NoSuchMessageException
。 -
String getMessage(MessageSourceResolvable resolvable, Locale locale)
:上述方法中使用的所有属性也都封装在一个名为MessageSourceResolvable
的类中,您可以在此方法中使用该类。
加载ApplicationContext
时,它会自动搜索上下文中定义的 MessageSource bean
。 bean
名称必须指定为 messageSource。如果找到这样的 bean
,则对上述方法的所有调用都将委托给消息源。如果未找到消息源,则 ApplicationContext
会尝试查找包含同名 bean
的父级。如果是,则使用该 bean
作为 MessageSource
。如果 ApplicationContext
找不到任何消息源,则会实例化一个空的 DelegatingMessageSource
,以便能够接受对上面定义的方法的调用。
Spring 提供了三个 MessageSource
实现,ResourceBundleMessageSource
、ReloadableResourceBundleMessageSource
和 StaticMessageSource
。它们都实现 HierarchicalMessageSource
以进行嵌套消息传递。 StaticMessageSource
很少使用,但提供了向源添加消息的编程方式。
实现
添加国际化文件
# greet.properties
greet=hello {0}
# greet_en_US.properties
greet=hello {0}
# greet.properties
你好啊 {0}
测试
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
@Configuration
public class AppConfig {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
String message = context.getMessage("illegal.argument", new Object[]{"taskId"}, "xxx", Locale.US);
System.out.println(message);
String greet = context.getMessage("greet", new Object[]{"打工人"}, Locale.CHINESE);
System.out.println(greet);
// 当无法找到code时,使用默认值
greet = context.getMessage("greet111", null, "你好啊 陌生人", Locale.CHINESE);
System.out.println(greet);
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// 添加国际化文件基础名
messageSource.addBasenames("greet");
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
return messageSource;
}
}
控制台输出:
你好啊 打工人
你好啊 陌生人
进阶
自定义查找路径
上一节介绍了如何配置和读取国际化, 以默认的方式, 比如, 文件的查找路径, 默认是classpath:
, 如果需要配置自定义路径, 比如, 我希望在classpath:/i18n
目录下寻找国际化文件, 那么只需要在messageSource.addBasenames
方法的参数加上路径前缀即可:
messageSource.addBasenames("classpath:/i18n/greet");
举一反三, 你也可以配置WEB-INF
文件夹下的资源.
追加BeanNames
由于MessageSource
是单例的, 很可能被封装在基础组件中(这意味着不能随意修改MessageSource
对象创建时的代码/配置), 那么当业务扩展, 我们需要添加更多资源时, 如何解决.
考虑使用BeanPostProcessor
:
@Component("msbpp")
public class MessageSourceBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof AbstractResourceBasedMessageSource) {
AbstractResourceBasedMessageSource ms = (AbstractResourceBasedMessageSource) bean;
ms.addBasenames("classpath:/i18n/greet");
}
return bean;
}
}
或者考虑可扩展性, 我们可以约定/预留一个配置文件如ms.locations
, 其中专门配置路径, 然后在postProcessAfterInitialization
方法中遍历追加.
Web应用中的Locale
上面的例子中, 获取国际化信息时, Locale
参数都是写死的, 但一般大型网站都是支持多语言的, 并且可以在前台切换, 那么, 如何感知并传递Locale
就是个问题, Spring
提供了LocaleChangeInterceptor
拦截器的方式, 用于动态更新当前请求所包含的Locale
信息, 我们只需要在配置MVC
时将此拦截器注册进去即可.
当需要获取时, 使用LocaleContextHolder
获取Locale
实例.
LocaleChangeInterceptor的工作原理
默认通过获取请求参数locale
(http://xxx.xxx/yyy/zzz?locale=xx_YY)得到当前请求的locale
信息, 通过LocaleResolver
实例将其解析/保存, 伪代码:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
String newLocale = request.getParameter(getParamName());
this.localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
return true;
}
查看setLocale
的实现, 有两个:
但可用的只有CookieLocaleResolver
, 因为AcceptHeaderLocaleResolver
不支持此方法, 否则会报错:
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
throw new UnsupportedOperationException(
"Cannot change HTTP accept header - use a different locale resolution strategy");
}
CookieLocaleResolver
可以将locale
其写入响应Cookie
, 以便后续请求可以直接从Cookie
中获取
也可以通过LocaleContextHolder.getLocale()
获取
配置与使用
配置
@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
// 忽视无效locale
interceptor.setIgnoreInvalidLocale(true);
registry.addInterceptor(interceptor).addPathPatterns("/*");
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(5000);
}
@Bean
public LocaleResolver localeResolver(){
CookieLocaleResolver acceptHeaderLocaleResolver = new CookieLocaleResolver();
acceptHeaderLocaleResolver.setDefaultLocale(Locale.CHINA);
return acceptHeaderLocaleResolver;
}
}
使用
@Resource MessageSource messageSource;
messageSource.getMessage("greet", new Object[]{"developer"}, LocaleContextHolder.getLocale());
效果
注意事项
- 本示例采用
UTF8
编码, 如果页面出现乱码, 可以下载Chrome
的编码插件
您可以在Gitee上找到本文的源代码。