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 ;
    }
上一篇下一篇

猜你喜欢

热点阅读