松哥以前写过 Spring Boot 国际化的问题,不过那一次没讲源码,此次我们整点源码来深刻理解下这个问题。html
国际化,也叫 i18n,为啥叫这个名字呢?由于国际化英文是 internationalization ,在 i 和 n 之间有 18 个字母,因此叫 i18n。咱们的应用若是作了国际化就能够在不一样的语言环境下,方便的进行切换,最多见的就是中文和英文之间的切换,国际化这个功能也是至关的常见。前端
仍是先来讲说用法,再来讲源码,这样你们不容易犯迷糊。咱们先说在 SSM 中如何处理国际化问题。java
首先国际化咱们可能有两种需求:web
大体上就是上面这两种场景。接下来松哥经过一个简单的用法来和你们演示下具体玩法。spring
首先咱们在项目的 resources 目录下新建语言文件,language_en_US.properties 和 language_zh-CN.properties,以下图:浏览器
内容分别以下:缓存
language_en_US.properties:session
login.username=Username login.password=Password
language_zh-CN.properties:mvc
login.username=用户名 login.password=用户密码
这两个分别对应英中文环境。配置文件写好以后,还须要在 SpringMVC 容器中提供一个 ResourceBundleMessageSource 实例去加载这两个实例,以下:app
<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,由于比较少见,松哥这里就不作过多介绍了。
Spring Boot 和 Spring 一脉相承,对于国际化的支持,默认是经过 AcceptHeaderLocaleResolver 解析器来完成的,这个解析器,默认是经过请求头的 Accept-Language 字段来判断当前请求所属的环境的,进而给出合适的响应。
因此在 Spring Boot 中作国际化,这一块咱们能够不用配置,直接就开搞。
首先建立一个普通的 Spring Boot 项目,添加 web 依赖便可。项目建立成功后,默认的国际化配置文件放在 resources 目录下,因此咱们直接在该目录下建立四个测试文件,以下:
四个文件建立好以后,第一个默认的咱们能够先空着,另外三个分别填入如下内容:
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?
有的小伙伴以为切换参数放在请求头里边好像不太方便,那么也能够自定义解析方式。例如参数能够当成普通参数放在地址栏上,经过以下配置能够实现咱们的需求。
@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 也是相似用法,再也不赘述。
默认状况下,咱们的配置文件放在 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 文件。
国际化这块主要涉及到的组件是 LocaleResolver,这是一个开放的接口,官方默认提供了四个实现。当前该使用什么环境,主要是经过 LocaleResolver 来进行解析的。
LocaleResolver
public interface LocaleResolver { Locale resolveLocale(HttpServletRequest request); void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale); }
这里两个方法:
咱们来看看 LocaleResolver 的继承关系:
虽然中间有几个抽象类,不过最终负责实现的其实就四个:
接下来咱们就对这几个类逐一进行分析。
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); }
Accept-Language
字段,则直接返回默认的 Locale。再来看看它的 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"); }
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 中便可。你们能够看到,这种保存方式其实和咱们前面演示的本身保存代码基本一致,异曲同工。
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"); }
CookieLocaleResolver 和 SessionLocaleResolver 比较相似,只不过存储介质变成了 Cookie,其余都差很少,松哥就再也不重复介绍了。
搜刮了一个语言简称表,分享给各位小伙伴:
语言 | 简称 |
---|---|
简体中文(中国) | 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 |
好啦,今天主要和小伙伴们聊了下 SpringMVC 中的国际化问题,以及 LocaleResolver 相关的源码,相信你们对 SpringMVC 的理解应该又更近一步了吧。