一、原理
Spring MVC使用国际化步骤:
1、编写国际化配置文件;
2、使用ResourceBundleMessageSource管理国际化资源文件;
3、在页面使用fmt:message取出国际化内容;
Spring Boot使用自动配置来配置国际化功能(Spring Boot有关自动配置都在spring-boot-autoconfigure.jar下)。找到org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.class(不同版本可能包路径不同),这个类就是国际化的自动配置类。主要是messageSource方法:
@Configuration
@ConditionalOnMissingBean(value = MessageSource.class, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
//添加MessageSourceProperties配置类
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
//新建了ResourceBundleMessageSource对象
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());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
...
}
public class MessageSourceProperties {
/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
//配置文件基础名(去掉语言国家代码的)我们的配置文件可以直接放在类路径下叫messages.properties
//可以自己配置,例如:spring.messages.basename=i18n.login,那么我们的配置文件就是放在i18n文件夹下的login.properties
//以及加上语言国家代码的例如:login_en_US.properties、login_zh_CN.properties等
private String basename = "messages";
/**
* Message bundles encoding.
*/
//设置编码,和我们.properties文件的编码一致即可,可以自己配置。spring.messages.encoding
private Charset encoding = StandardCharsets.UTF_8;
...
}
二、使用
使用Spring Boot做国际化功能步骤:
1、编写配置文件。
login_zh_CN.properties配置:
login.tip=请登录
login.username=用户名
login.password=密码
login.remember=记住我
login.btn=登陆
其他配置按照这个来,换成相应语言环境的语言即可
其中login.properties是默认配置,没有区域信息时使用这个其他两个一个是英文环境一个是中文环境使用。所以配置文件里的配置名相同,信息则根据语言环境来。
2、SpringBoot自动配置好了管理国际化资源文件的组件,我们只需配置好配置文件路径即可。
spring.messages.basename=i18n.login
spring.messages.encoding=utf-8
3、使用Thymeleaf的#{}表达式在页面上取出国际化信息。
<body class="text-center">
<form class="form-signin" action="dashboard.html" th:action="@{/user/login}" method="post">
<img class="mb-4" src="asserts/img/bootstrap-solid.svg" th:src="@{/asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" name="userName" th:placeholder="#{login.username}" placeholder="Username" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" placeholder="Password" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> [[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2017-2018</p>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</form>
</body>
至此,国际化配置算是基本完成了。效果:根据浏览器语言设置,页面自动切换成相应的语言环境。
三、自己设置语言环境
国际化原理:
国际化配置之所以会生效,是因为在程序中有一个Locale(区域信息对象),它保存了程序的语言环境信息,然后Spring Boot通过LocaleResolver来获取区域信息对象,LocaleResolver就是国际化生效的重要组件。我们可以写自己的LocaleResolver,然后使国际化显示按照我们自己的意愿来。
找到MVC的自动配置类org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.class,找到有关于LocaleResolver的配置;
@Bean
@ConditionalOnMissingBean//容器中没有这个bean才会生效。
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties
.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
这段代码可以看出如果我们在配置文件中配置了locale就返回FixedLocaleResolver对象,否则就会返回一个AcceptHeaderLocaleResolver对象。这个对象有一个resolveLocale方法
@Override
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = getDefaultLocale();//获取系统默认区域信息
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
}
Locale requestLocale = request.getLocale();//从reques获取也就是浏览器的区域信息
List<Locale> supportedLocales = getSupportedLocales();
if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
return requestLocale;
}
Locale supportedLocale = findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
}
return (defaultLocale != null ? defaultLocale : requestLocale);
}
这段代码说明AcceptHeaderLocaleResolver是从浏览器获取的区域信息对象,如果没有就要系统默认的。
自己设置国际化:
1、我们可以在需要国际化的页面(例如:index.html)上加上如下链接:
<a th:href="@{/index.html(l='zh_CN')}">中文</a>
<a th:href="@{/index.html(l='en_US')}">English</a>
给链接的传入区域信息参数,然后根据传入的参数设置区域信息对象。
2、Spring Boot在自动配置LocaleResolver时加了@ConditionalOnMissingBean注解,我们可以自己写一个LocaleResolver类,这样就加载自己LocaleResolver,默认的不生效。示例:
/**
* 添加自己的区域信息解析器
*
* @author Administrator
*
*/
public class MyLocaleResolver implements LocaleResolver {
private static final String I18N_LANGUAGE_SESSION = "i18n_language_session";
@Override
public Locale resolveLocale(HttpServletRequest request) {
// TODO Auto-generated method stub
String language = request.getParameter("l");
Locale locale = request.getLocale();
if (!StringUtils.isEmpty(language)) {
String[] spilt = language.split("_");
//第一个参数为语言信息,第二个为国家信息
locale = new Locale(spilt[0], spilt[1]);
// 将国际化语言保存到session
HttpSession session = request.getSession();
session.setAttribute(I18N_LANGUAGE_SESSION, locale);
} else {
// 如果没有带国际化参数,使用session中的locale,若没有,则使用request中的
HttpSession session = request.getSession();
Locale localeInSession = (Locale) session.getAttribute(I18N_LANGUAGE_SESSION);
if (localeInSession != null) {
locale = localeInSession;
}
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// TODO Auto-generated method stub
}
}
然后写个配置类把MyLocaleResolver 个加入到容器中,才能生效。
@Configuration
public class MyConfig{
//方法名必须为localeResolver,否则必须给@bean加上name="localeResolver"属性,否则不生效
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
注意:这个Bean的名字必须为localeResolver,否则Spring Boot不使用这个Bean作为默认的localeResolver,还是加载默认的。
原理:
找到spring-webmvc.jar里的org.springframework.web.servlet.DispatcherServlet类,找到initLocaleResolver方法,
(无关的代码就没截取)
@SuppressWarnings("serial")
public class DispatcherServlet extends FrameworkServlet {
public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized
// by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
}
}
private void initLocaleResolver(ApplicationContext context) {
try {
//先找名为localeResolver,类型是LocaleResolver.class的Bean
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.localeResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.localeResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
//没到到则调用getDefaultStrategy
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("No LocaleResolver '" + LOCALE_RESOLVER_BEAN_NAME +
"': using default [" + this.localeResolver.getClass().getSimpleName() + "]");
}
}
}
protected <T> T getDefaultStrategy(ApplicationContext context, Class<T> strategyInterface) {
List<T> strategies = getDefaultStrategies(context, strategyInterface);
...
return strategies.get(0);
}
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
catch (ClassNotFoundException ex) {
throw new BeanInitializationException(
"Could not find DispatcherServlet's default strategy class [" + className +
"] for interface [" + key + "]", ex);
}
catch (LinkageError err) {
throw new BeanInitializationException(
"Unresolvable class definition for DispatcherServlet's default strategy class [" +
className + "] for interface [" + key + "]", err);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}
}
从这段代码中可以知道如果找不到名为"localeResolver",类型是LocaleResolver.class的Bean,就根DispatcherServlet.properties配置文件返回一个默认的localeResolver,也就是AcceptHeaderLocaleResolver。
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
所以想要自己的LocaleResolver生效,加入容器时的名字必须是localeResolver。