Spring resource bundle多語言,單引號format異常
前言
十一假期被通知出現大bug,然后發現是多語言翻譯問題。法語中有很多單引號,單引號在format的時候出現無法匹配問題。這個問題是由spring resource bundle 並調用MessageFormat引起的,根本原因是MessageFormat會轉義單引號。
創建一個簡單的多語言demo,重現異常
1.配置
@Bean
public ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("msg");
source.setDefaultEncoding("UTF-8");
source.setFallbackToSystemLocale(false);
return source;
}
這里沒有指定localeResolver, 默認會使用AcceptHeaderLocaleResolver,也就是說從request的header中獲取Accept-Language
來解析語言。
ResourceBundleMessageSource是多語言翻譯的邏輯處理。source.setBasenames("msg")
綁定一個多語言的集合。這里我創建一個叫做msg的集合:
.
2.創建多語言方法
在main下右鍵創建一個文件夾i18n,然后將其設置為resources類型。在gradle中,可以在build.gradle里添加:
sourceSets {
main {
resources {
srcDir 'src/main/i18n'
}
}
}
然后-New-Resource Bundle. 起一個集合的名字,比如msg, 添加需要的語言包。
在里面添加內容
#msg.properties
user.name=default for en_US, I'm {0}
user.age='18'
#msg_en_US.properties
user.name=test, the user's name is {0}.
#msg_fr_FR.properties
user.name=This is french, I'm {0}
#msg_zh_CN.properties
user.name=測試 ,用戶名是 '{0}'
3.編寫一個controller測試
@Autowired
private MessageSource messageSource;
@ResponseBody
@RequestMapping(value = "/i18n/{name}", method = RequestMethod.GET)
public Map resource(Locale locale,
@PathVariable("name") String name){
Map map = new HashMap();
String[] arr = {name};
String message = messageSource.getMessage("user.name", arr, locale);
String age = messageSource.getMessage("user.age", null, locale);
map.put("username", message);
map.put("age", age);
return map;
}
- java中通過MessageSource來獲取配置語言包中內容
- 在controller的參數中添加Locale會自動注入LocaleResolver解析后的Locale, 當前是采用默認的AcceptHeaderLocaleResolver。當然也可以自己添加locale攔截器來自定義locale, 這個后面再去設置。
- 本例中獲取name和age。其中name需要插入參數,而age不需要參數,原樣輸出即可。
如果在jsp中可以使用spring標簽:
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<spring:message code="user.name" arguments="Ryan"/>
<spring:message code="user.age"/>
4.訪問
通過postman來訪問get請求:
- 可以看到age的單引號會原樣輸出,但name的單引號沒了,不僅如此,參數也並沒有傳入
- 這是因為messageSource在getMessage的時候采用了兩種策略,一種是原樣輸出,一種是采用MessageFormat來處理參數。
- 因此,主要原因就是MessageFormat的問題了。
5.測試MessageFormat
@Test
public void testQuote() throws Exception{
String message = "I'm {0}.";
String ryan = MessageFormat.format(message, "Ryan");
System.out.println(ryan);
Assert.assertEquals("Im {0}.", ryan);
message = "I''m {0}.";
ryan = MessageFormat.format(message, "Ryan");
System.out.println(ryan);
Assert.assertEquals("I'm Ryan.", ryan);
}
通過測試用例可以發現,MessageFormat會轉義(escape)單引號(quote)。因此,如果想要輸出一個單引號就需要針對的用兩個單引號來替換。
所以,解決上述問題的關鍵就是在語言包中涉及單引號的地方都做一下轉義,即兩個單引號。然而,這個步驟會比較繁瑣,而且會使得語言包的內容和顯示的內容不一致。因此,最好可以通過一個工具來將單引號自動轉義。
6.設置單引號轉義
既然已經知道問題原因所在了,那么只要在Format之前做一下轉義就可以了。
追蹤getMessage方法到AbstractMessageSource可以發現有參數和無參數的不同處理:
Object[] argsToUse = args;
if(!this.isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
String commonMessages1 = this.resolveCodeWithoutArguments(code, locale);
if(commonMessages1 != null) {
return commonMessages1;
}
} else {
argsToUse = this.resolveArguments(args, locale);
MessageFormat commonMessages = this.resolveCode(code, locale);
if(commonMessages != null) {
synchronized(commonMessages) {
return commonMessages.format(argsToUse);
}
}
}
那么,按道理,我們只要處理有參數的情況下就好了。接下來就應該是重寫resolveCode方法,將取出來的結果中的單引號替換。
要重寫的就是ResourceBundleMessageSource類, 但是發現這些方法都是私有的。這是因為我當前spring的版本是4.1.1。 意外升級成4.3.2之后發現這些方法已經變成protected。
接着發現由於私有成員變量能重寫的是getStringOrNull方法,但重寫后也會影響無參數的獲取。所以,設置ResourceBundleMessageSource
source.setAlwaysUseMessageFormat(true);
將所有的語言包獲取都走傳參路線,即都會經過MessageFormat處理,即單引號都要轉義。如此,便可以重寫getStringOrNull了。
創建ResourceFormat
public class ResourceFormat extends ResourceBundleMessageSource {
@Override
protected String getStringOrNull(ResourceBundle bundle, String key) {
if(bundle.containsKey(key)) {
try {
String val = bundle.getString(key);
return val.replaceAll("'","''");
} catch (MissingResourceException var4) {
;
}
}
return null;
}
}
然后修改配置類:
@Bean
public ResourceBundleMessageSource messageSource(){
ResourceBundleMessageSource source = new ResourceFormat();
source.setBasenames("msg");
source.setDefaultEncoding("UTF-8");
source.setFallbackToSystemLocale(false);
source.setAlwaysUseMessageFormat(true);
return source;
}
這樣,再次訪問:
這樣就正常了,單引號可以顯示,並且參數可以傳進去。
后記
關於locale resolver有多個實現類,通常使用SessionLocaleResolver
, 這時候需要添加一個攔截器,來將locale注入進去。注入locale的方法有很多,比如header,比如url直接傳參,比如cookie。通過各種手段獲取瀏覽器的語言之后,設置到locale里就可以了。
spring自帶了一個LocaleChangeInterceptor
,可以將參數locale攔截並注入。
因此,只要自己在攔截器里設置:
Locale langLocale = Locale.forLanguageTag(localeString);
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
localeResolver.setLocale(request, response, langLocale);
request.setAttribute("javax.servlet.jsp.jstl.fmt.locale", langLocale);
- localeString就是語言代碼,比如en-US, zh-CN