松哥之前写过 Spring Boot 国际化的问题,不过那一次没讲源码,这次咱们整点源码来深刻了解下这个问题。
国际化,也叫 i18n,为啥叫这个名字呢?因为国际化英文是 internationalization,在 i 和 n 之间有 18 个字母,所以叫 i18n。咱们的利用如果做了国际化就能够在不同的语言环境下,不便的进行切换,最常见的就是中文和英文之间的切换,国际化这个性能也是相当的常见。
1.SpringMVC 国际化配置
还是先来说说用法,再来说源码,这样大家不容易犯迷糊。咱们先说在 SSM 中如何解决国际化问题。
首先国际化咱们可能有两种需要:
- 在页面渲染时实现国际化(这个借助于 Spring 标签实现)
- 在接口中获取国际化匹配后的音讯
大抵上就是下面这两种场景。接下来松哥通过一个简略的用法来和大家演示下具体玩法。
首先咱们在我的项目的 resources 目录下新建语言文件,language_en_US.properties 和 language_zh-CN.properties,如下图:
内容别离如下:
language_en_US.properties:
login.username=Username
login.password=Password
language_zh-CN.properties:
login.username= 用户名
login.password= 用户明码
这两个别离对应英中文环境。配置文件写好之后,还须要在 SpringMVC 容器中提供一个 ResourceBundleMessageSource 实例去加载这两个实例,如下:
<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource">
<property name="basename" value="language"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
这里配置了文件名 language 和默认的编码格局。
接下来咱们新建一个 login.jsp 文件,如下:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<spring:message code="login.username"/> <input type="text"> <br>
<spring:message code="login.password"/> <input type="text"> <br>
</body>
</html>
在这个文件中,咱们通过 spring:message
标签来援用变量,该标签会依据以后的理论状况,抉择适合的语言文件。
接下来咱们为 login.jsp 提供一个控制器:
@Controller
public class LoginController {
@Autowired
MessageSource messageSource;
@GetMapping("/login")
public String login() {String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
System.out.println("username =" + username);
System.out.println("password =" + password);
return "login";
}
}
控制器中间接返回 login 视图即可。
另外我这还注入了 MessageSource 对象,次要是为了向大家展现如何在处理器中获取国际化后的语言文字。
配置实现后,启动我的项目进行测试。
默认状况下,零碎是依据申请头的中 Accept-Language 字段来判断以后的语言环境的,该这个字段由浏览器主动发送,咱们这里为了测试不便,能够应用 POSTMAN 进行测试,而后手动设置 Accept_Language 字段。
首先测试中文环境:
而后测试英文环境:
都没问题,完满!同时察看 IDEA 控制台,也能正确打印出语言文字。
下面这个是基于 AcceptHeaderLocaleResolver 来解析出以后的区域和语言的。
有的时候,咱们心愿语言环境间接通过申请参数来传递,而不是通过申请头来传递,这个需要咱们通过 SessionLocaleResolver 或者 CookieLocaleResolver 都能够实现。
先来看 SessionLocaleResolver。
首先在 SpringMVC 配置文件中提供 SessionLocaleResolver 的实例,同时配置一个拦截器,如下:
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="locale"/>
</bean>
</mvc:interceptor>
</mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.SessionLocaleResolver" id="localeResolver">
</bean>
SessionLocaleResolver 是负责区域解析的,这个没啥好说的。拦截器 LocaleChangeInterceptor 则次要是负责参数解析的,咱们在配置拦截器的时候,设置了参数名为 locale(默认即此),也就是说咱们未来能够通过 locale 参数来传递以后的环境信息。
配置实现后,咱们还是来拜访方才的 login 控制器,如下:
此时咱们能够间接通过 locale 参数来管制以后的语言环境,这个 locale 参数就是在后面所配置的 LocaleChangeInterceptor 拦截器中被主动解析的。
如果你不想配置 LocaleChangeInterceptor 拦截器也是能够的,间接本人手动解析 locale 参数而后设置 locale 也行,像上面这样:
@Controller
public class LoginController {
@Autowired
MessageSource messageSource;
@GetMapping("/login")
public String login(String locale,HttpSession session) {if ("zh-CN".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("zh", "CN"));
} else if ("en-US".equals(locale)) {session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("en", "US"));
}
String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
System.out.println("username =" + username);
System.out.println("password =" + password);
return "login";
}
}
SessionLocaleResolver 所实现的性能也能够通过 CookieLocaleResolver 来实现,不同的是前者将解析进去的区域信息保留在 session 中,而后者则保留在 Cookie 中。 保留在 session 中,只有 session 没有发生变化,后续就不必再次传递区域语言参数了,保留在 Cookie 中,只有 Cookie 没变,后续也不必再次传递区域语言参数了 。
应用 CookieLocaleResolver 的形式很简略,间接在 SpringMVC 中提供 CookieLocaleResolver 的实例即可,如下:
<bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" id="localeResolver"/>
留神这里也须要应用到 LocaleChangeInterceptor 拦截器,如果不应用该拦截器,则须要本人手动解析并配置语言环境,手动解析并配置的形式如下:
@GetMapping("/login3")
public String login3(String locale, HttpServletRequest req, HttpServletResponse resp) {CookieLocaleResolver resolver = new CookieLocaleResolver();
if ("zh-CN".equals(locale)) {resolver.setLocale(req, resp, new Locale("zh", "CN"));
} else if ("en-US".equals(locale)) {resolver.setLocale(req, resp, new Locale("en", "US"));
}
String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale());
String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale());
System.out.println("username =" + username);
System.out.println("password =" + password);
return "login";
}
配置实现后,启动我的项目进行测试,这次测试的形式跟 SessionLocaleResolver 的测试形式统一,松哥就不再多说了。
除了后面介绍的这几种 LocaleResolver 之外,还有一个 FixedLocaleResolver,因为比拟少见,松哥这里就不做过多介绍了。
2.Spring Boot 国际化配置
2.1 根本应用
Spring Boot 和 Spring 一脉相承,对于国际化的反对,默认是通过 AcceptHeaderLocaleResolver 解析器来实现的,这个解析器,默认是通过申请头的 Accept-Language 字段来判断以后申请所属的环境的,进而给出适合的响应。
所以在 Spring Boot 中做国际化,这一块咱们能够不必配置,间接就开搞。
首先创立一个一般的 Spring Boot 我的项目,增加 web 依赖即可。我的项目创立胜利后,默认的国际化配置文件放在 resources 目录下,所以咱们间接在该目录下创立四个测试文件,如下:
- 咱们的 message 文件是间接创立在 resources 目录下的,IDEA 在展现的时候,会多出一个 Resource Bundle,这个大家不必管,千万别手动去创立这个目录。
- messages.properties 这个是默认的配置,其余的则是不同语言环境下的配置,en_US 是英语 (美国),zh_CN 是中文简体,zh_TW 是中文繁体(文末附录里边有一个残缺的语言简称表格)。
四个文件创建好之后,第一个默认的咱们能够先空着,另外三个别离填入以下内容:
messages_zh_CN.properties
user.name= 江南一点雨
messages_zh_TW.properties
user.name= 江南壹點雨
messages_en_US.properties
user.name=javaboy
配置实现后,咱们就能够间接开始应用了。在须要应用值的中央,间接注入 MessageSource 实例即可。
在 Spring 中须要配置的 MessageSource 当初不必配置了,Spring Boot 会通过
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
主动帮咱们配置一个 MessageSource 实例。
创立一个 HelloController,内容如下:
@RestController
public class HelloController {
@Autowired
MessageSource messageSource;
@GetMapping("/hello")
public String hello() {return messageSource.getMessage("user.name", null, LocaleContextHolder.getLocale());
}
}
在 HelloController 中咱们能够间接注入 MessageSource 实例,而后调用该实例中的 getMessage 办法去获取变量的值,第一个参数是要获取变量的 key,第二个参数是如果 value 中有占位符,能够从这里传递参数进去,第三个参数传递一个 Locale 实例即可,这相当于以后的语言环境。
接下来咱们就能够间接去调用这个接口了。
默认状况下,在接口调用时,通过申请头的 Accept-Language 来配置以后的环境,我这里通过 POSTMAN 来进行测试,后果如下:
小伙伴们看到,我在申请头中设置了 Accept-Language 为 zh-CN,所以拿到的就是简体中文;如果我设置了 zh-TW,就会拿到繁体中文:
是不是很 Easy?
2.2 自定义切换
有的小伙伴感觉切换参数放在申请头里边如同不太不便,那么也能够自定义解析形式。例如参数能够当成一般参数放在地址栏上,通过如下配置能够实现咱们的需要。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang");
registry.addInterceptor(interceptor);
}
@Bean
LocaleResolver localeResolver() {SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
}
}
在这段配置中,咱们首先提供了一个 SessionLocaleResolver 实例,这个实例会替换掉默认的 AcceptHeaderLocaleResolver,不同于 AcceptHeaderLocaleResolver 通过申请头来判断以后的环境信息,SessionLocaleResolver 将客户端的 Locale 保留到 HttpSession 对象中,并且能够进行批改(这意味着以后环境信息,前端给浏览器发送一次即可记住,只有 session 无效,浏览器就不用再次通知服务端以后的环境信息)。
另外咱们还配置了一个拦截器,这个拦截器会拦挡申请中 key 为 lang 的参数(不配置的话是 locale),这个参数则指定了以后的环境信息。
好了,配置实现后,启动我的项目,拜访形式如下:
咱们通过在申请中增加 lang 来指定以后环境信息。这个指定只须要一次即可,也就是说,在 session 不变的状况下,下次申请能够不用带上 lang 参数,服务端曾经晓得以后的环境信息了。
CookieLocaleResolver 也是相似用法,不再赘述。
2.3 其余自定义
默认状况下,咱们的配置文件放在 resources 目录下,如果大家想自定义,也是能够的,例如定义在 resources/i18n 目录下:
然而这种定义形式零碎就不晓得去哪里加载配置文件了,此时还须要 application.properties 中进行额定配置 (留神这是一个相对路径):
spring.messages.basename=i18n/messages
另外还有一些编码格局的配置等,内容如下:
spring.messages.cache-duration=3600
spring.messages.encoding=UTF-8
spring.messages.fallback-to-system-locale=true
spring.messages.cache-duration 示意 messages 文件的缓存生效工夫,如果不配置则缓存始终无效。
spring.messages.fallback-to-system-locale 属性则略显神奇,网上居然看不到一个明确的答案,起初翻了一会源码才看出端倪。
这个属性的作用在 org.springframework.context.support.AbstractResourceBasedMessageSource#getDefaultLocale
办法中失效:
protected Locale getDefaultLocale() {if (this.defaultLocale != null) {return this.defaultLocale;}
if (this.fallbackToSystemLocale) {return Locale.getDefault();
}
return null;
}
从这段代码能够看出,在找不到以后零碎对应的资源文件时,如果该属性为 true,则会默认查找以后零碎对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到零碎默认的 messages.properties 文件。
3.LocaleResolver
国际化这块次要波及到的组件是 LocaleResolver,这是一个凋谢的接口,官网默认提供了四个实现。以后该应用什么环境,次要是通过 LocaleResolver 来进行解析的。
LocaleResolver
public interface LocaleResolver {Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
这里两个办法:
- resolveLocale:依据以后申请解析器出 Locale 对象。
- 设置 Locale 对象。
咱们来看看 LocaleResolver 的继承关系:
尽管两头有几个抽象类,不过最终负责实现的其实就四个:
- AcceptHeaderLocaleResolver:依据申请头中的 Accept-Language 字段来确定以后的区域语言等。
- SessionLocaleResolver:依据申请参数来确定区域语言等,确定后会保留在 Session 中,只有 Session 不变,Locale 对象就始终无效。
- CookieLocaleResolver:依据申请参数来确定区域语言等,确定后会保留在 Cookie 中,只有 Session 不变,Locale 对象就始终无效。
- FixedLocaleResolver:配置时间接提供一个 Locale 对象,当前不能批改。
接下来咱们就对这几个类逐个进行剖析。
3.1 AcceptHeaderLocaleResolver
AcceptHeaderLocaleResolver 间接实现了 LocaleResolver 接口,咱们来看它的 resolveLocale 办法:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale defaultLocale = getDefaultLocale();
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {return defaultLocale;}
Locale requestLocale = request.getLocale();
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);
}
- 首先去获取默认的 Locale 对象。
- 如果存在默认的 Locale 对象,并且申请头中没有设置
Accept-Language
字段,则间接返回默认的 Locale。 - 从 request 中取出以后的 Locale 对象,而后查问出反对的 supportedLocales,如果 supportedLocales 或者 supportedLocales 中蕴含 requestLocale,则间接返回 requestLocale。
- 如果后面还是没有匹配胜利的,则从 request 中取出 locales 汇合,而后再去和反对的 locale 进行比对,抉择匹配胜利的 locale 返回。
- 如果后面都没能返回,则判断 defaultLocale 是否为空,如果不为空,就返回 defaultLocale,否则返回 defaultLocale。
再来看看它的 setLocale 办法,间接抛出异样,意味着通过申请头解决 Locale 是不容许批改的。
@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");
}
3.2 SessionLocaleResolver
SessionLocaleResolver 的实现多了一个抽象类 AbstractLocaleContextResolver,AbstractLocaleContextResolver 中减少了对 TimeZone 的反对,咱们先来看下 AbstractLocaleContextResolver:
public abstract class AbstractLocaleContextResolver extends AbstractLocaleResolver implements LocaleContextResolver {
@Nullable
private TimeZone defaultTimeZone;
public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {this.defaultTimeZone = defaultTimeZone;}
@Nullable
public TimeZone getDefaultTimeZone() {return this.defaultTimeZone;}
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = resolveLocaleContext(request).getLocale();
return (locale != null ? locale : request.getLocale());
}
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
}
}
能够看到,多了一个 TimeZone 属性。从申请中解析出 Locale 还是调用了 resolveLocaleContext 办法,该办法在子类中被实现,另外调用 setLocaleContext 办法设置 Locale,该办法的实现也在子类中。
咱们来看下它的子类 SessionLocaleResolver:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName);
if (locale == null) {locale = determineDefaultLocale(request);
}
return locale;
}
间接从 Session 中获取 Locale,默认的属性名是 SessionLocaleResolver.class.getName() + ".LOCALE"
,如果 session 中不存在 Locale 信息,则调用 determineDefaultLocale 办法去加载 Locale,该办法会首先找到 defaultLocale,如果 defaultLocale 不为 null 就间接返回,否则就从 request 中获取 Locale 返回。
再来看 setLocaleContext 办法,就是将解析进去的 Locale 保存起来。
@Override
public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
@Nullable LocaleContext localeContext) {
Locale locale = null;
TimeZone timeZone = null;
if (localeContext != null) {locale = localeContext.getLocale();
if (localeContext instanceof TimeZoneAwareLocaleContext) {timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();}
}
WebUtils.setSessionAttribute(request, this.localeAttributeName, locale);
WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);
}
保留到 Session 中即可。大家能够看到,这种保留形式其实和咱们后面演示的本人保留代码基本一致,必由之路。
3.3 FixedLocaleResolver
FixedLocaleResolver 有三个构造方法,无论调用哪一个,都会配置默认的 Locale:
public FixedLocaleResolver() {setDefaultLocale(Locale.getDefault());
}
public FixedLocaleResolver(Locale locale) {setDefaultLocale(locale);
}
public FixedLocaleResolver(Locale locale, TimeZone timeZone) {setDefaultLocale(locale);
setDefaultTimeZone(timeZone);
}
要么本人传 Locale 进来,要么调用 Locale.getDefault() 办法获取默认的 Locale。
再来看 resolveLocale 办法:
@Override
public Locale resolveLocale(HttpServletRequest request) {Locale locale = getDefaultLocale();
if (locale == null) {locale = Locale.getDefault();
}
return locale;
}
这个应该就不必解释了吧。
须要留神的是它的 setLocaleContext 办法,间接抛异样进去,也就意味着 Locale 在前期不能被批改。
@Override
public void setLocaleContext( HttpServletRequest request, @Nullable HttpServletResponse response,
@Nullable LocaleContext localeContext) {throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");
}
3.4 CookieLocaleResolver
CookieLocaleResolver 和 SessionLocaleResolver 比拟相似,只不过存储介质变成了 Cookie,其余都差不多,松哥就不再反复介绍了。
4. 附录
搜刮了一个语言简称表,分享给各位小伙伴:
语言 | 简称 |
---|---|
简体中文 (中国) | zh_CN |
繁体中文 (中国台湾) | zh_TW |
繁体中文 (中国香港) | zh_HK |
英语 (中国香港) | en_HK |
英语 (美国) | en_US |
英语 (英国) | en_GB |
英语 (寰球) | en_WW |
英语 (加拿大) | en_CA |
英语 (澳大利亚) | en_AU |
英语 (爱尔兰) | en_IE |
英语 (芬兰) | en_FI |
芬兰语 (芬兰) | fi_FI |
英语 (丹麦) | en_DK |
丹麦语 (丹麦) | da_DK |
英语 (以色列) | en_IL |
希伯来语 (以色列) | he_IL |
英语 (南非) | en_ZA |
英语 (印度) | en_IN |
英语 (挪威) | en_NO |
英语 (新加坡) | en_SG |
英语 (新西兰) | en_NZ |
英语 (印度尼西亚) | en_ID |
英语 (菲律宾) | en_PH |
英语 (泰国) | en_TH |
英语 (马来西亚) | en_MY |
英语 (阿拉伯) | en_XA |
韩文 (韩国) | ko_KR |
日语 (日本) | ja_JP |
荷兰语 (荷兰) | nl_NL |
荷兰语 (比利时) | nl_BE |
葡萄牙语 (葡萄牙) | pt_PT |
葡萄牙语 (巴西) | pt_BR |
法语 (法国) | fr_FR |
法语 (卢森堡) | fr_LU |
法语 (瑞士) | fr_CH |
法语 (比利时) | fr_BE |
法语 (加拿大) | fr_CA |
西班牙语 (拉丁美洲) | es_LA |
西班牙语 (西班牙) | es_ES |
西班牙语 (阿根廷) | es_AR |
西班牙语 (美国) | es_US |
西班牙语 (墨西哥) | es_MX |
西班牙语 (哥伦比亚) | es_CO |
西班牙语 (波多黎各) | es_PR |
德语 (德国) | de_DE |
德语 (奥地利) | de_AT |
德语 (瑞士) | de_CH |
俄语 (俄罗斯) | ru_RU |
意大利语 (意大利) | it_IT |
希腊语 (希腊) | el_GR |
挪威语 (挪威) | no_NO |
匈牙利语 (匈牙利) | hu_HU |
土耳其语 (土耳其) | tr_TR |
捷克语 (捷克共和国) | cs_CZ |
斯洛文尼亚语 | sl_SL |
波兰语 (波兰) | pl_PL |
瑞典语 (瑞典) | sv_SE |
西班牙语 (智利) | es_CL |
5. 小结
好啦,明天次要和小伙伴们聊了下 SpringMVC 中的国际化问题,以及 LocaleResolver 相干的源码,置信大家对 SpringMVC 的了解应该又更近一步了吧。