1.前言
互聯網業務出海,將已有的業務Copy to Global,并且開始對各個國家精細化,本土化的運營,對于開發人員來說,國際化很重要,在實際專案中所要承擔的職責是按照客戶指定的語言讓服務端回傳相應語言的內容,本文基于spring的國際化支持,實作國際化的開箱即用,靜態檔案配置重繪生效以及全域例外國際化處理,
2.spring·i18n
ApplicationContext介面繼承了MessageSource介面,因此對外提供了internationalization(i18n)國際化的能力,如下就是常用的國際化中訊息轉換的三個方法:
public interface MessageSource {
//通過code檢索對應Locale的訊息,如果找不到就使用defaultMessage作為默認值
@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
//通過code檢索對應Locale的訊息,如果找不到會拋出例外,NoSuchMessageException
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
//和上面的方法其實本質是一樣的,只是通過resolvable去包裝了code,argument,defaultMessage,
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
?
在spring初始化之后,如果能在容器中找到messageSource的bean,會使用它進行訊息決議轉換,如果找不到,spring自己會實體化一個DelegatingMessageSource,不過這個物件中所有的方法都是空實作,還是需要有具體的實作去做事情,
而MessageSource介面有三個主要的實作類:
ResourceBundleMessageSource,ReloadableResourceBundleMessageSource,StaticMessageSource,
3.StaticMessageSource
3.1 簡單使用
StaticMessageSource,靜態記憶體訊息源,使用的比較少,他主要通過編碼的形式添加國際化映射對,可以在專案啟動時,手動注入一個StaticMessageSource,
@Bean
StaticMessageSource messageSource(){
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage("test1",Locale.CHINESE,"{0} 開始測驗");
messageSource.addMessage("test1",Locale.ENGLISH,"{0} start");
return messageSource;
}
3.2 AOP動態化從DB中加載國際化配置
自定義一個MineStaticMessageSource,借助StaticMessageSource的可編碼能力,可以簡單實作從資料庫中加載所有的配置資訊,并且注入到國際化配置中生效,如下:
專案啟動時就從DB中獲取所有的國際化配置資訊,組裝好后全部注入到MineStaticMessageSource中,
@Component
public class MineStaticMessageSource extends StaticMessageSource implements InitializingBean {
?
@Autowired
private StaticMessageService staticMessageService;
@Override
public void afterPropertiesSet() throws Exception {
List<StaticMessageDTO> staticMessages= staticMessageService.all();
for (StaticMessageDTO staticMessage : staticMessages) {
addMessage(staticMessage.getCode(),staticMessage.getLocale(),staticMessage.getMessage());
}
}
}
如何實作資料庫更改并動態感知重繪呢?那就要在資料庫中配置修改時能感知到,并且通知到自定義的這個訊息物件去重新初始化國際化配置,有如下方案經供參考:
-
通過AOP切面,攔截所有修改(增刪改)國際化配置的方法,在資料入庫成功之后,通過spring自帶的事件機制進行通知,可以使用
@AfterReturning環繞,并針對不同的code進行重新組裝資料, -
實作上彎彎繞繞的,需要做很多編碼實作,而且需要考慮事務問題,例外問題,所有的資料都在
StaticMessageSource的國際化map中,實際上我們并不能去洗掉一個國際化配置,使用以下的addMessage增改配置是沒有問題的,private final Map<String, Map<Locale, MessageHolder>> messageMap = new HashMap<>(); ? public void addMessage(String code, Locale locale, String msg) { Assert.notNull(code, "Code must not be null"); Assert.notNull(locale, "Locale must not be null"); Assert.notNull(msg, "Message must not be null"); this.messageMap.computeIfAbsent(code, key -> new HashMap<>(4)).put(locale, new MessageHolder(msg, locale)); if (logger.isDebugEnabled()) { logger.debug("Added message [" + msg + "] for code [" + code + "] and Locale [" + locale + "]"); } }
4.ResourceBundleMessageSource
ResourceBundleMessageSource,資源包訊息源,通過在專案的classpath中定義多個filename.properties,然后在創建ResourceBundleMessageSource時將定義的檔案名都注入到其中的basenameSet屬性中,專案啟動就可以把檔案中的配置讀取翻譯展示,
4.1 簡單使用
創建ResourceBundleMessageSource并注入到spring容器中,
@Bean
ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("test-i18n");
return messageSource;
}
創建test-i18n.properties檔案:
test.message=hello,world!
測驗成功:
@RequestMapping("/get")
public String get(String code, HttpServletRequest request) {
return messageSource.getMessage(code,null,request.getLocale());
}
?
//回傳值 hello,world!
4.2 原始碼決議·熱加載靜態檔案
ResourceBundleMessageSource對于訊息的決議處理時,對于Basenames中的多個檔案會依次創建對應的ResourceBundle,并根據code回傳對應的message,做一個實驗,專案啟動之后,對配置的靜態檔案中的配置熱修改,再請求一次,值會發生變化嗎?
不會,因為ResourceBundleMessageSource中有快取機制,對于前文說的創建的ResourceBundle會根據Basename進行快取,系統啟動之后,就快取了所有的ResourceBundle,快取結構是:Basename中包含<Locate,ResourceBundle>,
那么,如何實作動態加載修改過的靜態檔案呢?從原始碼中我們可以看到:
private final Map<String, Map<Locale, ResourceBundle>> cachedResourceBundles =
new ConcurrentHashMap<>();
?
if (getCacheMillis() >= 0) {
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
// do its native caching, at the expense of more extensive lookup steps.
return doGetBundle(basename, locale);
}
else {
// Cache forever: prefer locale cache over repeated getBundle calls.
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
}
有個屬cacheMillis性控制了是否會走快取,當cacheMillis大于0時,每次都不會走快取,重新生成ResourceBundle,那么最基本的優化點就是如下了,
@Bean
ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("test-i18n");
messageSource.setCacheSeconds(10);
return messageSource;
}
實作的效果是每次進入快取判斷分支時都會不走快取,重新生成ResourceBundle,也就實作了動態加載靜態檔案的效果,
4.3 不同語言的國際化配置
以上只是通過ResourceBundle讀取了properties檔案,并決議message回傳,實際專案使用中會根據各個國家,各個語言版本進行單獨的配置,做到對外輸出的國際化,比如,目前公司業務分布在中國,日本,菲律賓,一套后端服務要做到回傳資料的國際化,就需要按照一定的格式去配置,命名規范:自定義名_語言代碼_國別代碼.properties,比如:
test-i18n_zh_CN.properties
test-i18n_ja_JP.properties
test-i18n_en_PH.properties
值得注意的是:設定正確的編碼,banseName為前綴,
test-i18n.properties為基類配置,在代碼中實際上是ResourceBundle的父類,如果某個國家語言配置中不存在某個code,在父類中存在,那么也是可以正常獲取值的,
@Bean
ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasenames("test-i18n");
messageSource.setCacheMillis(1000L);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
5.ReloadableResourceBundleMessageSource
再聊聊ReloadableResourceBundleMessageSource,相比于上文的ResourceBundleMessageSource,有以下變化:
- 加載資源的方式不同:
ResourceBundleMessageSource通過 JDK 提供的 ResourceBundle 加載資源檔案;ReloadableResourceBundleMessageSource通過PropertiesPersister加載資源,支持xml、properties兩個格式,優先加載 properties 格式的檔案,如果同時存在 properties 和 xml 的檔案,會只加載 properties 的內容; - 靜態檔案的熱加載方式發生了變化,
cacheMillis引數作用發生了變化,
5.1 簡單使用
創建ReloadableResourceBundleMessageSource并注入到spring容器中,
@Bean
ReloadableResourceBundleMessageSource messageSource(){
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:test-i18n");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
創建test-i18n_zh_CN.properties檔案:
test.message=你好,世界!
測驗成功:
@RequestMapping("/get")
public String get(String code, HttpServletRequest request) {
return messageSource.getMessage(code,null,request.getLocale());
}
?
//回傳值 你好,世界!
5.2 原始碼決議·不一樣的快取引數
首先我們看一下快取部分的代碼:
if (getCacheMillis() < 0) {
PropertiesHolder propHolder = getMergedProperties(locale);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
else {
for (String basename : getBasenameSet()) {
List<String> filenames = calculateAllFilenames(basename, locale);
for (String filename : filenames) {
PropertiesHolder propHolder = getProperties(filename);
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
}
}
}
對于ReloadableResourceBundleMessageSource,設定messageSource.setCacheSeconds(10);的效果和前文說的ResourceBundleMessageSource的快取控制條件相同,只有設定為<0時[默認值為-1],才會進入快取流程,而大于0則走向了檔案加載&有條件重繪的流程,
@Bean
ReloadableResourceBundleMessageSource messageSource(){
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:test-i18n");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
不設定的話默認值為-1,并且有意思的是,因為是采用PropertiesPersister進行檔案的決議,所以快取的資料源就的國際化組態檔中的key-value鍵值對,根據locale去讀取所有的檔案名,并將所有的key-value鍵值對全部都快取到記憶體中的properties,同時使用locale進行路由不同的PropertiesHolder,
后續每次獲取message的時候,都會從這個大properties[merged properties]中嘗試獲取,找得到就回傳,找不到就拋例外,
//快取各個語言的mergedHolder
PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
//根據配置去讀取所有的檔案名
List<String> filenames = calculateAllFilenames(basenames[i], locale);
//從快取的properties中讀取code對應的配置
String result = propHolder.getProperty(code);
if (result != null) {
return result;
}
cacheMillis引數和前文ResourceBundleMessageSource不同點:
ResourceBundleMessageSource中cacheMillis只做了一件事,就是粗粒度地控制了是否走快取流程,并且對于本地靜態檔案的重繪是每一次都會重繪,- 而
ReloadableResourceBundleMessageSource中cacheMillis多了另一個職責-超時重繪靜態檔案,當不走快取流程時,會通過比對上次重繪時間和[當前時間-cacheMillis]的大小去選擇是否重新重繪本地的靜態檔案配置到記憶體中,
5.3 原始碼決議·雙重快取·重繪的奧義
從勺ò干知,設定messageSource.setCacheSeconds(10);
控制快取時間為10s,ReloadableResourceBundleMessageSource便具備了超時重繪的能力,
以下,originalTimestamp是上次properties重繪的時間戳,getCacheMillis()獲取的是cacheMillis,目前我們的配置是10s,以下代碼的判斷很清晰了,如果重繪時間是在【當前時間減去快取控制時間】之后,那么就直接使用原來的propHolder,不做重繪操作,
if (propHolder != null) {
originalTimestamp = propHolder.getRefreshTimestamp();
if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) {
// Up to date
return propHolder;
}
}
對于不走快取流程的分支,其中這里也有一個快取,這里的快取是根據所有的國際化組態檔名作為key的快取,而之前是通過Locate作為key進行快取,這是最大的區別,這樣做的好處就是,可以做到按檔案進行重繪,
PropertiesHolder propHolder = this.cachedProperties.get(filename);
原始碼閱讀中,一些小的技術細節也值得我們去品味,比如,對于每一個檔案持有物件propHolder內部都有一個ReentrantLock,在多執行緒環境下,也能保證只有一個執行緒去進行檔案讀寫重繪,這就保證了費時的操作可以盡可能地由單執行緒完成,
private final ReentrantLock refreshLock = new ReentrantLock();
?
propHolder.refreshLock.lock();
?
try {
PropertiesHolder existingHolder = this.cachedProperties.get(filename);
if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
return existingHolder;
}
return refreshProperties(filename, propHolder);
}
finally {
propHolder.refreshLock.unlock();
}
對于需要重繪的key,呼叫refreshProperties(filename, propHolder);完成重繪,重繪操作很簡單,從類路徑下讀取對應檔案名的靜態檔案,并裝載到記憶體中的properties中,同時設定檔案最后的更新時間lastModified到propHolder中,
Properties props = loadProperties(resource, filename);
propHolder = new PropertiesHolder(props, fileTimestamp);
并且可以看到,設定本次重繪的時間戳,重新創建新的propHolder,并設定到快取結構cachedProperties中去,完成本次的重繪,
propHolder.setRefreshTimestamp(refreshTimestamp);
this.cachedProperties.put(filename, propHolder);
以上,完成的效果就是:對于國際化的配置,當獲取
message時,如果本地靜態檔案修改之后,只要超過10秒就會重繪重新加載最新的配置資訊到快取中,
6.全域例外處理的國際化配置
業務對外跑出的例外,是國際化轉換最重要的出口處,對于全域例外處理的方案老生常談了,只需要使用幾個注解就可以勝任,
@Slf4j
@ControllerAdvice
public class RestExceptionHandler {
?
@ExceptionHandler(value = https://www.cnblogs.com/bingo24/p/BaseBizException.class)
public CommonResult
那么如何結合以上我們的i18n的messageSource達成國際化轉換呢?只需要稍稍改造就能完成,
- 全域例外處理類中注入
messageSource - 業務例外處理方法新增
Locale引數,他是國際化轉換的路由因子, - 使用
messageSource的getMessage做國際化翻譯,其中我們也可以把引數都帶進來,這樣就能做到引數化的國際化翻譯, - 最后就是吐出去,給親愛的用戶了,
@Autowired
MessageSource messageSource;
?
@ExceptionHandler(value = https://www.cnblogs.com/bingo24/p/BaseBizException.class)
public CommonResult
7.后續的思考
通過本地國際化語言靜態檔案可以實作多個語言的配置,并且配合快取和檔案重繪機制也能做到系統運行中的熱更新,但是,現實中,我們很多服務都做了微服務部署,一個系統有多個實體,那么這種檔案的形式就有了挑戰,要么一個個去改服務器上的檔案,要么就是通過一些統一掛載盤的形式去實作檔案統一修改,但這些都不是最優解,還容易出錯,再看看輪子們,現在有了nacos,有了apollo,這些配置中心都具有遠程配置,中心化存盤,可監聽(實時更新)的能力,我們可以考慮結合這些輪子去改造spring的i18n實作,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/545684.html
標籤:Java
