這篇文章讓你搞懂 SpringMVC 國際化!

松哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小夥伴戳這裏-->Spring Boot+Vue+微人事視頻教程


松哥之前寫過 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);

}

這裏兩個方法:

  1. resolveLocale:根據當前請求解析器出 Locale 對象。
  2. 設置 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);
}
  1. 首先去獲取默認的 Locale 對象。
  2. 如果存在默認的 Locale 對象,並且請求頭中沒有設置 Accept-Language 字段,則直接返回默認的 Locale。
  3. 從 request 中取出當前的 Locale 對象,然後查詢出支持的 supportedLocales,如果 supportedLocales 或者 supportedLocales 中包含 requestLocale,則直接返回 requestLocale。
  4. 如果前面還是沒有匹配成功的,則從 request 中取出 locales 集合,然後再去和支持的 locale 進行比對,選擇匹配成功的 locale 返回。
  5. 如果前面都沒能返回,則判斷 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 的理解應該又更近一步了吧。

本文分享自微信公衆號 - 江南一點雨(a_javaboy)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章