國際化的支援,對於app開發的小夥伴來說應該比價常見了;作為java後端的小夥伴,一般來講接觸國際化的機會不太多,畢竟業務開展到海外的企業並沒有太多
SpringBoot提供了國際化的支援,網上也有相關的教程,然而實際體驗的時候,發現並沒有預期的那麼順利;本文將介紹一下SpringBoot如何支援國家化,以及在支援的過程中,一些注意事項
I. 專案環境
1. 專案依賴
本專案藉助SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發
開一個web服務用於測試
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
2. 配置檔案
配置檔案中,指定國際化的引數,thmeleaf的配置資訊
application.yml
spring:
messages:
basename: i18n/messages/messages
encoding: UTF-8
fallbackToSystemLocale: false
thymeleaf:
mode: HTML
encoding: UTF-8
servlet:
content-type: text/html
cache: false
3. 國際化資訊檔案
上面的配置 spring.messages.basename
指定國際化配置檔案的目錄與字首,取值為i18n/messages/messages
所以在資源目錄下,新建檔案 i18n/messages
,國際化檔名為 messages-xxx.properties
,專案結果如
對應的資訊如簡體中文 messages_zh_CN.properties
200=成功
500=內部異常
name=使用者名稱
pwd=密碼
英文 messages_en_US.properties
200=success
500=unexpected exception
name=user name
pwd=password
繁體 messages_zh_TW.properties
200=成功
500=內部異常
name=使用者名稱
pwd=密碼
說明
注意spring.messages.basename
這個配置的取值為國際化檔案的目錄 + 檔名字首
,比如上面若少了最後一層的messages
,會提示取不到配置
其次在IDEA中,選中國家化檔案之後,點選下方的Resource Bundle
,可以進入如上圖中更友好的編輯框,支援一次修改多個語言的資訊
II. 國際化支援
前面是國際化的基本配置,那麼如何根據前面配置中的key,獲取不同語言的value呢?
1. MessageSource
在SpringBoot中主要藉助MessageSource
來獲取不同語言的value資訊
如一個最基本的封裝
public class MsgUtil {
private static MessageSource messageSource;
public static void inti(MessageSource messageSource) {
MsgUtil.messageSource = messageSource;
}
/**
* 獲取單個國際化翻譯值
*/
public static String get(String msgKey) {
try {
return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
} catch (Exception e) {
return msgKey;
}
}
}
2. 測試demo
接下來寫一個基礎的測試demo,根據傳參來修改LocalContextHolder
中的值,從而實現不同語言的切換
@Controller
@SpringBootApplication
public class Application {
public Application(MessageSource messageSource) {
MsgUtil.inti(messageSource);
}
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Data
@Accessors(chain = true)
public static class RspWrapper<T> {
private int code;
private String msg;
private T data;
}
@GetMapping(path = "change")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}
}
演示如下
3. 子執行緒支援
上面雖然可以根據請求引數來切換語言,但是有個問題,如果在子執行緒中進行國際化支援,則會不生效
@GetMapping(path = "change2")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}
如下圖,即便修改了language,返回都是預設的中文
針對這種解決辦法是在設定Locale時,指定第二個可繼承引數為true
@GetMapping(path = "change3")
@ResponseBody
public String changeLocal(String language) {
String[] s = language.split("_");
LocaleContextHolder.setLocale(new Locale(s[0], s[1]));
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(true);
return JSON.toJSONString(res);
}
4. Cookies方式快取國際化資訊
上面雖說支援了根據傳參來設定國際化,但是需要每次傳參都帶上這個引數language=zh_CN
,還需要我們自己來解析這個請求引數,我們可以考慮藉助攔截器來實現統一的Local設定
這個攔截器可以自己按照上面的方式寫,當然更推薦的是直接使用已封裝好的
@Configuration
public class AutoConfig implements WebMvcConfigurer {
/**
* 這個如果不存在,則會拋異常: nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution strategy
*
* @return
*/
@Bean
public LocaleResolver localeResolver() {
// 也可以換成 SessionLocalResolver, 區別在於國際化的應用範圍
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return localeResolver;
}
/**
* 根據請求引數,來設定本地化
*
* @return
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
// Defaults to "locale" if not set
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) {
interceptorRegistry.addInterceptor(localeChangeInterceptor());
}
}
請注意上面的 localResolver
, 當我們不註冊這個bean的時候,執行則會丟擲異常nested exception is java.lang.UnsupportedOperationException: Cannot change HTTP accept header - use a different locale resolution
上面的例項中,採用的是CookieLocaleResolver
,因此會在cookie中快取語言資訊,一次修改,後續都會生效
測試如下
@GetMapping(path = "say")
@ResponseBody
public String say(String name) {
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
return JSON.toJSONString(res);
}
@GetMapping(path = "say2")
@ResponseBody
public String say2(String name) {
RspWrapper res = new RspWrapper<>().setCode(200).setMsg(MsgUtil.get("200")).setData(MsgUtil.get("name") + ":" + name);
return JSON.toJSONString(res);
}
主要一個地方設定了語言,後續的訪問不帶語言引數時,都會複用之前設定的語言,這樣使用來說就更簡潔了
5. 頁面元素國際化
上面介紹的是返回的json串支援國際化,另外一個場景就是我們返回的頁面,希望渲染的資料也可以實現國際化支援
在上文的基礎上實現這個也沒什麼難度了
在資源目錄下,新建目錄templates
,新建模板檔案 index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>一灰灰blog 國際化測試頁面</title>
</head>
<body>
<div>
<div class="title">hello world!</div>
<br/>
<div class="content" th:text="'name: ' + ${name}">預設使用者名稱</div>
<br/>
<div class="sign" th:text="'pwd: ' + ${pwd}">預設密碼</div>
<br/>
</div>
</body>
</html>
對應的controller
@GetMapping(path = {"", "/", "/index"})
public String index(Model model) {
model.addAttribute("name", MsgUtil.get("name"));
model.addAttribute("pwd", MsgUtil.get("pwd"));
return "index";
}
雖說上面這樣實現了國家化的支援,但是看起來不太優雅,難道還需要後端介面進行轉義一下麼,沒有更簡單的方式麼?
Themeleaf提供了更簡單的支援方式,將上面的$改成#即可
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="YiHui"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>一灰灰blog 國際化測試頁面</title>
</head>
<body>
<div>
<div class="title">hello world!</div>
<br/>
<div class="content" th:text="'name: ' + #{name}">預設使用者名稱</div>
<br/>
<div class="sign" th:text="'pwd: ' + #{pwd}">預設密碼</div>
<br/>
<div class="content" th:text="'200: ' + #{200}">200</div>
<br/>
<div class="content" th:text="'500: ' + #{500}">500</div>
</div>
</body>
</html>
對應的rest
@GetMapping(path = "show")
public String show() {
return "show";
}
6. 注意事項
在實現國際化的過程中,遇到了下面幾個問題,特此記錄一下
6.1 配置資訊無法獲取
在使用messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale())
查詢配置資訊,結果提示org.springframework.context.NoSuchMessageException: No message found under code '200' for locale 'en_US'.
出現上面這個問題,當然優先判斷是否真的配置了這個引數,其次確認spring.messages.basename
是否準確,對應的value為目錄 + 語言的字首
- 如我的配置檔案為
i18n/messages/messages_en_US.properties
, 那麼這個value就應該是i18n/messages/messages
6.2 中文亂碼問題
- 設定編碼
spring.messages.encoding=utf-8
如果發現上面這個設定了依然沒有生效,那麼考慮一下配置檔案是否為utf-8編碼
6.3 根據請求支援國際化
需要新增本地化的攔截器LocaleChangeInterceptor
,來實現根據請求引數,解析語言環境
其次需要註冊LocaleResolver
,比如demo中使用CookieLocaleResolver
,來儲存國際化資訊 (如果不設定它會拋異常)
II. 其他
0. 專案
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 專案原始碼: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/150-i18n
1. 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人部落格 https://blog.hhui.top
- 一灰灰Blog-Spring專題部落格 http://spring.hhui.top