鬆哥以前寫過 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 的理解應該又更近一步了吧。