基於SpringCloud的enum列舉值國際化處理實踐
背景
選用SpringCloud框架搭建微服務做業務後臺應用時,會涉及到大量的業務狀態值定義,一般常規做法是:
- 持久層(資料庫)儲存int型別的值
- 後臺系統裡用閱讀性好一點兒的常量將int型別的值做一層對映
- 前端(app或瀏覽器)同樣定義一套常量去對映這些關係
- 前端呼叫後臺系統的介面時,使用常量定義的int型別進行提交
源於持久層儲存的優化規則,int型別要比varchar型別效率高很多,這套做法也是大家接受度非常高的。
只是這裡有一個不是很方便的地方:狀態值對映的常量定義涉及前端和後臺兩部分,溝通的成本是一方面,另外如果狀態值有變化,需要兩組人員同時修改。
預期目標
在保證持久層的int型別儲存狀態值的前提下,主要是考慮業務狀態的可閱讀性問題和多處修改的問題,可閱讀性問題一部分可以通過前後端人員定義常量來解決,但介面除錯時還是直接使用int型別,這部分的可閱讀性問題還是存在,多處修改的問題需要重點解決。
本篇推薦的方案:
- 持久層(資料庫)儲存沒用原先的int型別值,這點保持不變
- 後臺系統使用enum定義業務狀態,不同的業務狀態集可以由多個enum來實現,enum支援國際化
- 前端展示enum國際化的文字內容
- 前端呼叫後臺系統介面時,使用enum國際化的文字內容進行提交
- 後臺接收enum國際化的文字內容轉換成int型別值,儲存在資料庫
方案的優點:
- 持久層原有的設計,效率性問題不受影響
- 業務狀態的定義、對映全部內聚到後臺系統,後續有狀態值變化時,只需後臺做相應修改即可
- 前端展示的內容,介面傳輸的內容均為閱讀性更好的文字,並且支援國際化
方案的缺點:
- 後臺系統儲存、讀取狀態值時,需要用enum進行轉換
- 通訊傳輸的內容報文比原有的int型別大一點點
方案實踐
實踐原理
此實踐方案主要包含三部分:
- Enum類使用Jackson進行JSON序列化和反序列化
- Enum列舉項的messages國際化處理
- Enum的定義
Enum自定義序列化和反序列化
先定義Enum國際化類,自定義Enum的序列化和反序列化類,並使用註解@JsonSerialize、@JsonDeserialize註冊到Spring的ObjectMapper中
@JsonDeserialize(using = DescEnumDeserializer.class) @JsonSerialize(using = DescEnumSerializer.class) public interface I18NEnum { /** * 獲取列舉描述 * * @return */ String getDesc(); }
參考一下自定義的序列化實現:
/**
* @author huangying
*/
public class DescEnumSerializer extends JsonSerializer<I18NEnum> {
@Override
public void serialize(I18NEnum value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 按類名+列舉值名稱拼接配置檔案key,全部大寫處理
String key = value.getClass().getSimpleName() + "." + StringUtils.upperCase(value.toString());
// I18NUtil為國際化處理工具類
String data = I18NUtil.get(key, value.getDesc());
gen.writeString(data);
}
}
自定義的反序列化實現:
/**
* @author huangying
*/
public class DescEnumDeserializer extends JsonDeserializer<I18NEnum> {
@Override
public I18NEnum deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
JsonNode node = p.getCodec().readTree(p);
Class enumCls = BeanUtils.findPropertyType(p.currentName(), p.getCurrentValue().getClass());
List enumFields = EnumUtils.getEnumList(enumCls);
String keyPrefix = enumCls.getSimpleName() + ".";
for (Object enumField : enumFields) {
I18NEnum i18NEnum = (I18NEnum) enumField;
// I18NUtil為國際化處理工具類
String data = I18NUtil.get(keyPrefix + StringUtils.upperCase(i18NEnum.toString()), i18NEnum.getDesc());
if (node.asText().equals(data)) {
return i18NEnum;
}
}
throw new I18NEnumException("enum:未知的列舉型別");
}
}
自定義一個專用異常,這樣看起來更加高大上:
/**
* @author huangying
*/
public class I18NEnumException extends RuntimeException {
public I18NEnumException(String message) {
super(message);
}
}
國際化處理工具類
這個國際化處理的工具類是通用的,讀取專案工程裡的messages.properties\messages_zh_CN.properties\messages_en.properties等配置檔案的MessageSource資訊,並根據具體的語言,返回資訊來完成國際化顯示,程式碼如下:
/**
* @author huangying
*/
@Component
public class I18NUtil {
private static MessageSource messageSource;
public I18NUtil(MessageSource messageSource) {
I18NUtil.messageSource = messageSource;
}
public static String get(String key) {
return messageSource.getMessage(key, null, LocaleContextHolder.getLocale());
}
public static String get(String key, Object arg) {
return messageSource.getMessage(key, new Object[]{arg}, LocaleContextHolder.getLocale());
}
}
Enum定義示例
我們舉一個enum定義的示例,有SUCCESS和FAIL兩個列舉值,儲存在資料庫中的int值分別是1和2:
public enum OperateEnum implements I18NEnum {
/**
* 個人日常消費
*/
SUCCESS(1, "SUCCESS"),
/**
* 裝修
*/
FAIL(2,"FAIL");
private int index;
private String desc;
OperateEnum(int index, String desc) {
this.index = index;
this.desc = desc;
}
@Override
public String getDesc() {
return desc;
}
public int getIndex() {
return index;
}
}
配置檔案的寫法:
# messages.properties內容
# 列舉類
OperateEnum.SUCCESS=success
OperateEnum.FAIL=fail
# messages_zh_CN.properties內容
# 列舉類
OperateEnum.SUCCESS=操作成功
OperateEnum.FAIL=操作失敗
方案應用
在SpringCloud環境下,新增對國際化語言的處理,我們統一將國際語言標識放在request header的lang裡面:
/**
* @author huangying
*/
public class I18NLocalResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String lang = request.getHeader("lang");
//獲取jvm預設locale
Locale locale = Locale.getDefault();
if (lang != null) {
locale = new Locale(lang);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
自定義enum的序列化方法觸發
在接口裡只需要將enum類返回,在@ResponseBody進行處理時即可觸發enum國際化的序列化方法,示例介面如下:
@ApiOperation(value = "列舉值國際化示例")
@ApiImplicitParams({
@ApiImplicitParam(name = "uid", value = "操作人員ID", paramType = "header", dataType = "Long")})
@RequestMapping(value = "/test/enums", method = RequestMethod.GET)
public Result get(
@RequestHeader(value = "lang") String lang) {
return Result.success(EnumUtils.getEnumList(OperateEnum.class));
}
自定義enum的反序列化方法觸發
MappingJackson2HttpMessageConverter轉換器預設將@RequestBody的內容做反序列化處理,如果enum的國際化值傳遞給了客戶端,若需要正確處理客戶端提交的列舉值國際化內容,最簡單的辦法是將enum定義在@RequestBody的物件中,就能自動觸發enum的自定義反序列化方法,並得到期望的結果。
若在@RequestParam修飾的引數上定義enum物件,請求中的String轉換成enum是通過org.springframework.core.convert.support.StringToEnumConverterFactory 來實現的,該類實現了介面 ConverterFactory ,通過呼叫 Enum.valueOf(Class, String) 實現了這個功能,而不會觸發enum列舉值的反序列化。因此只能處理與列舉值相同的字面值(name),enum列舉值國際化處理後,可能與字面值不相同,直接使用@RequestParam來轉換,會報錯。
如果要讓@RequestParam能夠觸發enum列舉值的反序列化操作,可以嘗試重寫springmvc的引數轉換器,此處略。
小結
enum列舉值的國際化處理,是個非常有意思的改進,既可能解決閱讀性的問題,又提高了業務定義的內聚性,此方案的應用取決於前後端的編碼習慣,如果是在專案初期,前後端童鞋溝通確認後可以嘗試此方案,希望對你有幫助。
專注Java高併發、分散式架構,更多技術乾貨分享與心得,請關注公眾號:Java架構社群
可以掃左邊二維碼新增好友,邀請你加入Java架構社群微信群共同探討技術