2021-12-15 服务端多语言阶段总结
2021-12-17 本文已影响0人
江江江123
业务需求
服务端返回的字段根据公共参数携带的语言返回对应结果,如果出现系统未支持的语言默认返回英语。
迭代史
第一版
返回数据中加入language字段:查询数据时通过language字段查询对应的数据。
优点:快速解决了多语言问题
缺点:随着业务增多,数据量变大,每次配置的数据越来越多,维护难度追加升高
第二版
将语言从业务中抽离,参考i18n建立数据库,数据记录所属语言,对应英语字符串做key,对应翻译做value。在查询到业务数据后,再查询翻译,使用${}字符串内部替换,或者直接使用字段value值做替换。
优点:只维护一套业务数据,多语言数据单独维护
缺点:每次在需要翻译的业务后面需要写大量的语言替换代码,工作量大
第三版
在第二版的基础上使用aop,需要翻译的业务块上只要加一些注解即可完成翻译。
优点:使用简单
缺点:部分性能损耗
技术点汇总
1.根据用户传入的language判断应该返回的语言
public class LangConstant {
public static final String DEFAULT = "en";
/**
* @param langList 支持的语言集合
* @param lang 用户传入的语言
* @title: getLang
* @return: java.lang.String 应该返回的语言
* @version: 1.0
* @description: 根据用户传入的语言返回系统支持的语言
*/
public static String getLang(Collection<String> langList, String lang) {
String[] split = lang.split("-");
if (split.length == 1) {
Optional<String> first = langList.stream().filter(langStr -> Objects.equals(langStr, lang)).findFirst();
if (first.isPresent()) {
return first.get();
} else {
return DEFAULT;
}
} else {
long count = langList.stream().filter(langStr -> Objects.equals(langStr, lang)).count();
if (count == 0) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < split.length - 1; i++) {
stringBuilder.append(split[i]).append("-");
}
return getLang(langList, stringBuilder.substring(0, stringBuilder.length() - 1).trim());
} else {
return langList.stream().filter(langStr -> Objects.equals(langStr, lang)).findFirst().get();
}
}
}
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("en");
strings.add("ja");
strings.add("ar");
strings.add("zh-Hans");
strings.add("zh-Hant");
strings.add("zh");
System.out.println(getLang(strings, "en-US"));
System.out.println(getLang(strings, "ja-JP"));
System.out.println(getLang(strings, "en-QA"));
System.out.println(getLang(strings, "zh-Hans-CN"));
System.out.println(getLang(strings, "zh-Hant-TW"));
System.out.println(getLang(strings, "zh-Hant"));
System.out.println(getLang(strings, "zh"));
}
}
2.${key} 的替换
方案一 commons-text
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
简单使用
Map<String, String> dict = new HashMap<>();
dict.put("world", "my world");
String key = "hello ${world}";
StringSubstitutor stringSubstitutor = new StringSubstitutor(dict);
String replaceKey = stringSubstitutor.replace(key);
优点:快速使用
缺点:对一些 ${}替换后仍然携带 ${}的数据不友好
方案二 正则替换
@Slf4j
public class PlaceholderUtils {
/**
* Prefix for system property placeholders: "${"
*/
public static final String PLACEHOLDER_PREFIX = "${";
/**
* Suffix for system property placeholders: "}"
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* @param text 待替换文本
* @param parameter 替换词库
* @title: resolvePlaceholders
* @return: java.lang.String 替换后文本
*/
public static String resolvePlaceholders(String text, Map parameter) {
if (StringUtils.isEmpty(text) || parameter == null || parameter.isEmpty()) {
return text;
}
StringBuffer buf = new StringBuffer(text);
int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
try {
String propVal = parameter.get(placeholder).toString();
if (propVal != null) {
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
nextIndex = startIndex + propVal.length();
} else {
log.warn("Could not resolve placeholder '" + placeholder + "' in [" + text + "] ");
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), placeholder);
}
} catch (Exception ex) {
log.warn("Could not resolve placeholder '" + placeholder + "' in [" + text + "]: " + ex);
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), placeholder);
}
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);
} else {
startIndex = -1;
}
}
return buf.toString();
}
}
优点:自定义强,有问题随时改
缺点:可能存在未知隐患,但目前使用暂无问题
3 spring aop + cache
注:这部分和业务有耦合,可优化改进
定义注解
所属项目
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccesskeyParam {
}
用户语言
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LanguageParam {
}
多语言替换方式,语言所属分类
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MultiLanguageTranslation {
InternationalizationType type() default InternationalizationType.DEFAULT;
ReplacementModel model() default ReplacementModel.FIELD;
enum ReplacementModel {
FIELD,
EXPRESSION
}
@ToString
@AllArgsConstructor
enum InternationalizationType {
DEFAULT("1"), SUBSCRIPTION("2"), ENTRY("3");
public final String code;
}
}
多语言接口(这块联用了cache)
不同项目场景下中可自行实现,比如可以选择从redis,mysql,i18n文件流中获取多语言数据
public interface MultiLanguageService {
@Cacheable("InternationalizationTypeCache")
Map<String, String> getMap(String accesskey, String type, String lang);
@CacheEvict(value = "InternationalizationTypeCache", allEntries = true)
void clearAll();
}
aop切面
@Aspect
@Configuration
@AutoConfigureAfter(MultiLanguageService.class)
@Slf4j
public class MultiLanguageTranslationAspectConfiguration {
@Autowired(required = false)
MultiLanguageService multiLanguageService;
private static Map<MultiLanguageTranslation.ReplacementModel, HandlerMultiLanguage> handlerMap = new HashMap<>();
static {
handlerMap.put(MultiLanguageTranslation.ReplacementModel.FIELD, new FieldSetLanguageValue());
handlerMap.put(MultiLanguageTranslation.ReplacementModel.EXPRESSION, new ExpressSetLanguageValue());
}
@Pointcut("@annotation(com.yongyi.utils.multiLanguage.MultiLanguageTranslation)")
private void lockPoint() {
}
@Around("lockPoint()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
MultiLanguageTranslation multiLanguageTranslation = method.getAnnotation(MultiLanguageTranslation.class);
Map<String, String> map = getMultiLanguageMap(pjp.getArgs(), method.getParameterAnnotations(), multiLanguageTranslation.type().code); //读取多语言字典
Object proceed = pjp.proceed();
//handler result
handlerResult(multiLanguageTranslation.model(), map, proceed);
return proceed;
}
/**
* @author: yangniuhaojiang
* @description: 对返回类型为Map, Collection, Object数据分别处理
*/
public void handlerResult(MultiLanguageTranslation.ReplacementModel model, Map<String, String> map, Object proceed) {
if (proceed instanceof Map) {
Map result = (Map) proceed;
result.values().forEach(mapValue -> handlerResult(model, map, mapValue));
} else if (proceed instanceof Collection) {
Collection list = (Collection) proceed;
list.forEach(obj -> handlerMap.get(model).setLanguageValue(map, obj));
} else {
//普通对象
handlerMap.get(model).setLanguageValue(map, proceed);
}
}
/**
* @author: yangniuhaojiang
* @description: 根据注解参数获取多语言翻译表
*/
private Map<String, String> getMultiLanguageMap(Object[] args, Annotation[][] parameterAnnotations, String type) {
String accesskey = "";
String lang = "en";
for (Annotation[] parameterAnnotation : parameterAnnotations) {
int paramIndex = ArrayUtils.indexOf(parameterAnnotations, parameterAnnotation);
for (Annotation annotation : parameterAnnotation) {
if (annotation instanceof AccesskeyParam) {
accesskey = (String) args[paramIndex];
} else if (annotation instanceof LanguageParam) {
lang = (String) args[paramIndex];
}
}
}
return multiLanguageService.getMap(accesskey, type, lang);
}
interface HandlerMultiLanguage {
void setLanguageValue(Map<String, String> map, Object obj);
}
/**
* @author: yangniuhaojiang
* @description: 字段替换
*/
static class FieldSetLanguageValue implements HandlerMultiLanguage {
@Override
public void setLanguageValue(Map<String, String> map, Object obj) {
Class<?> objClass = obj.getClass();
Field[] fields = getFields(objClass, objClass.getDeclaredFields());
for (Field field : fields) {
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value instanceof String) {
String valueStr = (String) value;
field.set(obj, map.getOrDefault(valueStr, valueStr));
}
} catch (IllegalAccessException e) {
log.error("get error");
}
}
}
}
/**
* @author: yangniuhaojiang
* @description: 表达式替换
*/
static class ExpressSetLanguageValue implements HandlerMultiLanguage {
@Override
public void setLanguageValue(Map<String, String> map, Object obj) {
Class<?> objClass = obj.getClass();
Field[] fields = getFields(objClass, objClass.getDeclaredFields());
for (Field field : fields) {
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value instanceof String) {
String valueStr = (String) value;
field.set(obj, PlaceholderUtils.resolvePlaceholders(valueStr, map));
}
} catch (IllegalAccessException e) {
log.error("get error");
}
}
}
}
/**
* @param objClass
* @param fields
* @title: getFields
* @return: java.lang.reflect.Field[]
* @description: 递归获取父类字段
*/
public static Field[] getFields(Class<?> objClass, Field[] fields) {
if (!objClass.isAssignableFrom(Object.class)) {
Class<?> superclass = objClass.getSuperclass();
Field[] declaredFields = superclass.getDeclaredFields();
fields = ArrayUtils.addAll(fields, declaredFields);
getFields(superclass, fields);
}
return fields;
}
}
案例
@Override
@MultiLanguageTranslation(type = MultiLanguageTranslation.InternationalizationType.ENTRY, model = MultiLanguageTranslation.ReplacementModel.FIELD)
public List<Entry> list(@AccesskeyParam String accesskey, @LanguageParam String oriLang, String buildVersion) {
List<Entry> entries = entryMapper.list(lang, buildVersion);
return entries ;
}