美文网首页
2021-12-15 服务端多语言阶段总结

2021-12-15 服务端多语言阶段总结

作者: 江江江123 | 来源:发表于2021-12-17 12:19 被阅读0次

    业务需求

    服务端返回的字段根据公共参数携带的语言返回对应结果,如果出现系统未支持的语言默认返回英语。

    迭代史

    第一版

    返回数据中加入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 ;
        }
    

    相关文章

      网友评论

          本文标题:2021-12-15 服务端多语言阶段总结

          本文链接:https://www.haomeiwen.com/subject/hqipfrtx.html