flea-common使用之本地国际化实现

引言

百度百科针对 国际化 的解释:

本地国际化,就是指应用程序根据所处语言环境的不同【如 Java 中可用 国际化标识类 java.util.Locale 区分不同语言环境】,自动匹配应用内置的相应的语言环境下的资源配置【如 Java 中可用 资源包类 java.util.ResourceBundle 来匹配】,从而获取并对外展示相应的语言环境下的资源信息。

话不多说,直接上干货:

1. 依赖

1
2
3
4
5
6
<!-- FLEA COMMON-->
<dependency>
<groupId>com.huazie.fleaframework</groupId>
<artifactId>flea-common</artifactId>
<version>2.0.0</version>
</dependency>

2. 实现

上面提到了 Java 中 的 国际化标识类 java.util.Locale资源包类 java.util.ResourceBundle,这两者就是本地国际化实现的关键所在。

2.1 定义国际化资源相关配置

flea-config.xml 用于特殊配置国际化资源的路径和文件前缀。

1
2
3
4
5
6
<flea-config>
<!-- flea-common -->
<config-items key="flea-i18n-config" desc="Flea国际化相关配置">
<config-item key="error" desc="error国际化资源特殊配置,指定路径和文件前缀,逗号分隔">flea/i18n,flea_i18n</config-item>
</config-items>
</flea-config>

2.2 定义Flea I18N 配置类

在使用 FleaI18nConfig 之前,我们先了解下Flea国际化资源文件的组成,主要有如下 5 部分:

上述国际化资源也可以配置默认资源文件,即文件名中不需要包含国际化标识 。例如: flea/i18n/flea_i18n_error.properties

注意: 国际化资源文件扩展名必须为 properties

好了,基础的认知有了,我们开始了解 FleaI18nConfig,如下贴出了实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
* Flea I18N 配置类,用于获取指定语言环境下的指定资源对应的国际化数据。
*
* <p> 它默认读取资源路径为 flea/i18n,资源文件前缀为 flea_i18n,当然
* 也可以在 flea-config.xml 中为指定资源文件配置路径和前缀,从而可以
* 实现读取任意位置的资源数据。
*
* @author huazie
* @version 2.0.0
* @since 1.0.0
*/
public class FleaI18nConfig {

private static final FleaLogger LOGGER = FleaLoggerProxy.getProxyInstance(FleaI18nConfig.class);

private static volatile FleaI18nConfig config;

private ConcurrentMap<String, String> resFilePath = new ConcurrentHashMap<>(); // 资源文件路径集

private ConcurrentMap<String, ResourceBundle> resources = new ConcurrentHashMap<>(); // 资源集

/**
* 只允许通过 getConfig() 获取 Flea I18N 配置类实例
*/
private FleaI18nConfig() {
init(); // 初始化资源文件相关配置
}

/**
* 获取 Flea I18N 配置类实例
*
* @return Flea I18N 配置类实例
* @since 1.0.0
*/
public static FleaI18nConfig getConfig() {
if (ObjectUtils.isEmpty(config)) {
synchronized (FleaI18nConfig.class) {
if (ObjectUtils.isEmpty(config)) {
config = new FleaI18nConfig();
}
}
}
return config;
}

/**
* 初始化资源名和资源文件相关属性的映射关系
*
* @since 1.0.0
*/
private void init() {
ConfigItems fleaI18nItems = FleaConfigManager.getConfigItems(CommonConstants.FleaI18NConstants.FLEA_I18N_CONFIG_ITEMS_KEY);
if (ObjectUtils.isNotEmpty(fleaI18nItems) && CollectionUtils.isNotEmpty(fleaI18nItems.getConfigItemList())) {
for (ConfigItem configItem : fleaI18nItems.getConfigItemList()) {
if (ObjectUtils.isNotEmpty(configItem) && StringUtils.isNotBlank(configItem.getKey()) && StringUtils.isNotBlank(configItem.getValue())) {
String[] valueArr = StringUtils.split(configItem.getValue(), CommonConstants.SymbolConstants.COMMA);
if (ArrayUtils.isNotEmpty(valueArr) && CommonConstants.NumeralConstants.INT_TWO == valueArr.length) {
// 获取资源文件路径
String filePath = StringUtils.trim(valueArr[0]);
// 获取资源文件前缀
String fileNamePrefix = StringUtils.trim(valueArr[1]);
if (StringUtils.isNotBlank(filePath) && StringUtils.isNotBlank(fileNamePrefix)) {
String configResFilePath;
// 如果资源文件路径最后没有 "/",自动添加
if (CommonConstants.SymbolConstants.SLASH.equals(StringUtils.subStrLast(filePath, 1))) {
configResFilePath = filePath + fileNamePrefix;
} else {
configResFilePath = filePath + CommonConstants.SymbolConstants.SLASH + fileNamePrefix;
}
resFilePath.put(configItem.getKey(), configResFilePath);
}
}
}
}
}
// 添加默认资源文件路径
String defaultResFilePath = CommonConstants.FleaI18NConstants.FLEA_I18N_FILE_PATH +
CommonConstants.FleaI18NConstants.FLEA_I18N_FILE_NAME_PREFIX; // 默认资源文件路径(仅包含公共的部分)
resFilePath.put(CommonConstants.SymbolConstants.ASTERISK, defaultResFilePath);
}

/**
* 通过国际化数据的key,获取当前系统指定资源的国际化资源;
* 其中国际化资源中使用 {} 标记的,需要values中的数据替换。
*
* @param key 国际化资源KEY
* @param values 待替换字符串数组
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化资源数据
* @since 2.0.0
*/
public FleaI18nData getI18NData(String key, String[] values, String resName, Locale locale) {
return new FleaI18nData(key, this.getI18NDataValue(key, values, resName, locale));
}

/**
* 通过国际化数据的key,获取当前系统指定资源的国际化资源
*
* @param key 国际化资源KEY
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化资源数据
* @since 1.0.0
*/
public FleaI18nData getI18NData(String key, String resName, Locale locale) {
return new FleaI18nData(key, this.getI18NDataValue(key, resName, locale));
}

/**
* <p> 通过国际化数据的key,获取当前系统指定资源的国际化资源数据 </p>
*
* @param key 国际化资源KEY
* @param values 国际化资源数据替换内容
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化资源数据
* @since 1.0.0
*/
public String getI18NDataValue(String key, String[] values, String resName, Locale locale) {
String value = getI18NDataValue(key, resName, locale);
if (ArrayUtils.isNotEmpty(values)) {
StringBuilder builder = new StringBuilder(value);
for (int i = 0; i < values.length; i++) {
StringUtils.replace(builder, CommonConstants.SymbolConstants.LEFT_CURLY_BRACE + i + CommonConstants.SymbolConstants.RIGHT_CURLY_BRACE, values[i]);
}
value = builder.toString();
}
return value;
}

/**
* <p> 通过国际化数据的key,获取当前系统指定资源的国际化资源数据 </p>
*
* @param key 国际化资源KEY
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化资源数据
* @since 1.0.0
*/
public String getI18NDataValue(String key, String resName, Locale locale) {
Object obj = null;
if (LOGGER.isDebugEnabled()) {
obj = new Object() {
};
LOGGER.debug1(obj, "Find the key : {}", key);
LOGGER.debug1(obj, "Find the resName : {}", resName);
LOGGER.debug1(obj, "Find the locale : {} , {}", locale == null ? Locale.getDefault() : locale, locale == null ? Locale.getDefault().getDisplayLanguage() : locale.getDisplayLanguage());
}
ResourceBundle resource = getResourceBundle(resName, locale);

String value = null;
if (ObjectUtils.isNotEmpty(resource)) {
value = resource.getString(key);
if (StringUtils.isBlank(value)) { // 如果取不到数据,则使用key返回
value = key;
}
}

if (LOGGER.isDebugEnabled()) {
LOGGER.debug1(obj, "Find the value : {} ", value);
}
return value;
}

/**
* <p> 根据资源名和国际化标识获取指定国际化配置ResourceBundle对象 </p>
*
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化配置ResourceBundle对象
* @since 1.0.0
*/
private ResourceBundle getResourceBundle(String resName, Locale locale) {

String key = generateKey(resName, locale);

Object obj = null;
if (LOGGER.isDebugEnabled()) {
obj = new Object() {
};
LOGGER.debug1(obj, "Find the resKey : {}", key);
}

ResourceBundle resource = resources.get(key);

// 获取资源文件名
StringBuilder fileName = new StringBuilder(getResFilePath(resName));
if (StringUtils.isNotBlank(resName)) {
fileName.append(CommonConstants.SymbolConstants.UNDERLINE).append(resName);
}

if (LOGGER.isDebugEnabled()) {
if (ObjectUtils.isEmpty(locale)) {
LOGGER.debug1(obj, "Find the expected fileName: {}.properties", fileName);
} else {
LOGGER.debug1(obj, "Find the expected fileName: {}_{}.properties", fileName, locale);
}
}

// 获取资源文件
if (ObjectUtils.isEmpty(resource)) {
if (ObjectUtils.isEmpty(locale)) {
resource = ResourceBundle.getBundle(fileName.toString());
} else {
resource = ResourceBundle.getBundle(fileName.toString(), locale);
}
resources.put(key, resource);
}

if (LOGGER.isDebugEnabled()) {
Locale realLocale = resource.getLocale();
if (ObjectUtils.isEmpty(locale) || StringUtils.isBlank(realLocale.toString())) {
LOGGER.debug1(obj, "Find the real fileName: {}.properties", fileName);
} else {
LOGGER.debug1(obj, "Find the real fileName: {}_{}.properties", fileName, realLocale);
}
}

return resource;
}

/**
* <p> 获取国际化资源文件KEY </p>
* <p> 如果资源名不为空,则资源名作为key,同时如果国际化标识不为空,则取资源名+下划线+国际化语言作为key;
*
* @param resName 资源名
* @param locale 国际化标识
* @return 国际化资源文件KEY
* @since 1.0.0
*/
private String generateKey(String resName, Locale locale) {
String key = "";
if (StringUtils.isNotBlank(resName)) {
key = resName;
if (ObjectUtils.isNotEmpty(locale)) {
key += CommonConstants.SymbolConstants.UNDERLINE + locale;
}
}
return key;
}

/**
* <p> 根据资源名,获取资源文件路径 </p>
*
* @param resName 资源名
* @return 资源文件路径
* @since 1.0.0
*/
private String getResFilePath(String resName) {
// 首先根据资源名,从 资源文件路径集中获取
String resFilePathStr = resFilePath.get(resName);
if (ObjectUtils.isEmpty(resFilePathStr)) {
// 取默认资源文件路径
resFilePathStr = resFilePath.get(CommonConstants.SymbolConstants.ASTERISK);
}
return resFilePathStr;
}

}

2.3 定义Flea I18N 工具类

FleaI18nHelper 封装了 I18N 资源数据获取的静态方法,主要包含如下4种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static String i18n(String key, String resName, Locale locale) {
return FleaI18nConfig.getConfig().getI18NDataValue(key, resName, locale);
}

public static String i18n(String key, String[] values, String resName, Locale locale) {
return FleaI18nConfig.getConfig().getI18NDataValue(key, values, resName, locale);
}

// 实际在调用该方法之前,可以通过 FleaFrameManager.getManager().setLocale(Locale) 设置当前线程的国际化标识。
public static String i18n(String key, String resName) {
return i18n(key, resName, FleaFrameManager.getManager().getLocale());
}

// 实际在调用该方法之前,可以通过 FleaFrameManager.getManager().setLocale(Locale) 设置当前线程的国际化标识。
public static String i18n(String key, String[] values, String resName) {
return i18n(key, values, resName, FleaFrameManager.getManager().getLocale());
}

// 其他是对具体资源的封装,如错误码资源error、授权资源auth 和 公共信息资源common

2.4 定义Flea I18N资源枚举

FleaI18nResEnum 定义了 Flea I18N 的资源文件类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* Flea I18N 资源枚举
*
* @author huazie
* @version 1.0.0
* @since 1.0.0
*/
public enum FleaI18nResEnum {

ERROR("error", "异常信息国际码资源文件类型"),
ERROR_CORE("error_core", "FLEA CORE异常信息国际码资源文件类型"),
ERROR_DB("error_db", "FLEA DB异常信息国际码资源文件类型"),
ERROR_JERSEY("error_jersey", "FLEA JERSEY异常信息国际码资源文件类型"),
ERROR_AUTH("error_auth", "FLEA AUTH异常信息国际码资源文件类型"),
AUTH("auth", "FLEA AUTH 国际码资源文件类型"),
COMMON("common", "公共信息国际码资源文件类型");

private String resName;
private String resDesc;

/**
* <p> 资源文件类型枚举构造方法 </p>
*
* @param resName 资源名
* @param resDesc 资源描述
* @since 1.0.0
*/
FleaI18nResEnum(String resName, String resDesc) {
this.resName = resName;
this.resDesc = resDesc;
}

public String getResName() {
return resName;
}

public String getResDesc() {
return resDesc;
}

}

简单的介绍之后,初步了解了本地国际化的实现,下面就需要来实际测试一下了。

话不多说,开始操刀:

3. 自测

首先,我们先添加几个国际化配置文件,如下:

资源文件 国际化标识(语言环境)
flea/i18n/flea_i18n_error.properties 默认
flea/i18n/flea_i18n_error_zh_CN.properties 中文(简体)
flea/i18n/flea_i18n_error_en_US.properties 英文(美式)

注意: 笔者电脑的本地语言环境为 中文(简体)

3.1 匹配指定语言

1
2
3
4
5
@Test
public void fleaI18nHelperTest1() {
String value = FleaI18nHelper.i18n("ERROR0000000001", "error", Locale.US);
LOGGER.debug("Value = {}", value);
}

测试结果:

3.2 匹配本地语言

1
2
3
4
5
@Test
public void fleaI18nHelperTest() {
String value = FleaI18nHelper.i18n("ERROR0000000001", "error", Locale.FRANCE);
LOGGER.debug("Value = {}", value);
}

测试结果:

3.3 匹配默认资源

首先,我们将本地语言的资源文件删除,如下:

1
2
3
4
5
@Test
public void fleaI18nHelperTest() {
String value = FleaI18nHelper.i18n("ERROR0000000001", "error", Locale.FRANCE);
LOGGER.debug("Value = {}", value);
}

测试结果:

3.4 无资源匹配

首先,我们将本地语言默认 的 资源文件删除,如下:

1
2
3
4
5
@Test
public void fleaI18nHelperTest() {
String value = FleaI18nHelper.i18n("ERROR0000000001", "error", Locale.FRANCE);
LOGGER.debug("Value = {}", value);
}

测试结果:

4. 接入

上面演示了 如何通过 FleaI18nHelper 获取本地国际化的资源数据,下面我们来看看在异常类中接入错误码国际化资源。

4.1 定义通用异常类

CommonException 定义了 Flea I18N 下的通用异常,由子类传入具体的国际化资源枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* Flea I18N 通用异常,由子类传入具体的国际化资源枚举类型
*
* @author huazie
* @version 1.0.0
* @since 1.0.0
*/
public abstract class CommonException extends Exception {

private static final long serialVersionUID = 1746312829236028651L;

private String key; // 国际化资源数据关键字

private Locale locale; // 国际化区域标识

private FleaI18nResEnum i18nResEnum; // 国际化资源类型

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum) {
// 使用服务器当前默认的国际化区域设置
this(mKey, mI18nResEnum, FleaFrameManager.getManager().getLocale());
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, String... mValues) {
// 使用服务器当前默认的国际化区域设置
this(mKey, mI18nResEnum, FleaFrameManager.getManager().getLocale(), mValues);
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Locale mLocale) {
// 使用指定的国际化区域设置
this(mKey, mI18nResEnum, mLocale, new String[]{});
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Locale mLocale, String... mValues) {
// 使用指定的国际化区域设置
super(convert(mKey, mValues, mI18nResEnum, mLocale));
key = mKey;
locale = mLocale;
i18nResEnum = mI18nResEnum;
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Throwable cause) {
// 使用服务器当前默认的国际化区域设置
this(mKey, mI18nResEnum, FleaFrameManager.getManager().getLocale(), cause);
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Throwable cause, String... mValues) {
// 使用服务器当前默认的国际化区域设置
this(mKey, mI18nResEnum, FleaFrameManager.getManager().getLocale(), cause, mValues);
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Locale mLocale, Throwable cause) {
// 使用指定的国际化区域设置
this(mKey, mI18nResEnum, mLocale, cause, new String[]{});
}

public CommonException(String mKey, FleaI18nResEnum mI18nResEnum, Locale mLocale, Throwable cause, String... mValues) {
// 使用指定的国际化区域设置
super(convert(mKey, mValues, mI18nResEnum, mLocale), cause);
key = mKey;
locale = mLocale;
i18nResEnum = mI18nResEnum;
}

private static String convert(String key, String[] values, FleaI18nResEnum i18nResEnum, Locale locale) {
if (ObjectUtils.isEmpty(locale)) {
locale = FleaFrameManager.getManager().getLocale(); // 使用当前线程默认的国际化区域设置
}
if (ObjectUtils.isEmpty(i18nResEnum)) {
i18nResEnum = FleaI18nResEnum.ERROR; // 默认使用 国际化资源名为 error
}
if (ArrayUtils.isNotEmpty(values)) {
return FleaI18nHelper.i18n(key, values, i18nResEnum.getResName(), locale);
} else {
return FleaI18nHelper.i18n(key, i18nResEnum.getResName(), locale);
}
}

public String getKey() {
return key;
}

public Locale getLocale() {
return locale;
}

public FleaI18nResEnum getI18nResEnum() {
return i18nResEnum;
}
}

4.2 定义业务逻辑层异常类

ServiceException 定义了业务逻辑层抛出的异常,对应的国际化资源名为【error】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 业务逻辑层异常类,定义了业务逻辑层抛出的异常,
* 其对应的国际化资源名为【error】
*
* @author huazie
* @version 1.0.0
* @since 1.0.0
*/
public class ServiceException extends CommonException {

public ServiceException(String key) {
super(key, FleaI18nResEnum.ERROR);
}

public ServiceException(String key, String... values) {
super(key, FleaI18nResEnum.ERROR, values);
}

public ServiceException(String key, Throwable cause) {
super(key, FleaI18nResEnum.ERROR, cause);
}

public ServiceException(String key, Throwable cause, String... values) {
super(key, FleaI18nResEnum.ERROR, cause, values);
}

}

总结

好了,Flea框架下的本地国际化实现已经介绍完毕,欢迎大家使用!