很多场景下,咱们开发的项目需要进行语言的切换,但在每个接口中去通过获取请求头中的语言标识。查询数据时,根据语言标识过滤掉不匹配的字段,只查询与当前语言标识匹配的字段和公用字段。会导致大量冗余代码,许多与业务无关的代码会混入业务逻辑中,增加维护难度和开发成本。

      所以想到通过拦截器的方式,在查询底层数据的时候,不需要关心语言类型,查询返回时,拦截器中对接口返回的数据进行国际化的处理,从而实现动态数据国际化的处理。

      比如现在有一个web端项目,在登录的时候通过选择语言,对应进入不同语言版本的系统,显示不同语言的菜单管理。那么,系统菜单表sys_menu表中设计的字段包括:

字段名称 字段类型 字段描述
menu_code varchar(20) 菜单编码
title varchar(20) 菜单名称
cn_title varchar(20) 菜单中文名称
en_title varchar(20) 菜单英文名称
th_title varchar(20) 菜单泰文名称

使用拦截器和JsonNode树模型进行统一处理,如下:

创建一个拦截器,对所有controller层的接口请求进行处理:

   1、创建一个名为MultiLanguageAdvice的类,实现ResponseBodyAdvice接口,用于拦截响应数据。在beforeBodyWrite方法中,通过JsonNode树模型对所有需要国际化处理的数据进行解析和处理,递归遍历响应结构。

   2、在代码中维护一个语言前缀列表,使用时将其转换为小写,用于后面过滤带有语言前缀的字段,使用HttpServletRequest获取到请求头中的语言前缀language,将其从语言前缀列表中移除,再将body转换为JsonNode,递归调用自定义的方法responseDataParseAndRemove()进行字段移除和重组。

@Slf4j
@RestControllerAdvice
public class MultiLanguageAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 通过HttpServletRequest获取到用户当前的语言环境
        ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes());
        if (servletRequestAttributes != null) {
            HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
            // 本地维护一个语言前缀列表
            List<String> languageList = new ArrayList<>();
            languageList.add("CN");//中文
            languageList.add("EN");//英文
            languageList.add("TH");//泰文
            // 将响应数据body序列化为JsonNode
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode node = objectMapper.readTree(objectMapper.writeValueAsString(body));

            //接口的header中传的有语言参数,才做这种特殊处理
            String language = httpServletRequest.getHeader("language");
            if (StringUtils.isNotBlank(language)){
                languageList.remove(language);
                responseDataParseAndRemove(node, languageList, language);
            }
            return node;
        }
        return body;
    }

}

  3、在自定义方法responseDataParseAndRemove()中递归遍历所有字段,移除带有指定语言前缀的字段并新建目标字段,将当前语言标识的字段对应的值赋值给目标字段。

/**
* 响应数据解析和指定前缀字段移除
* @param node 响应数据节点
* @param toRemoveFieldPrefixList 待移除的字段前缀数组
* @description: 通过解析Json结构的响应数据、移除指定前缀字段、以达到动态数据在不同语言环境下的动态切换
**/
public static void responseDataParseAndRemove(JsonNode node, List<String> toRemoveFieldPrefixList, String language) {
    // 节点只有两种:容器节点和非容器节点
    if (node.isContainerNode()) {
        // 判断该节点是对象还是数组
        if (node.isObject()) {
            List<String> currentLanguageFieldNameList = new ArrayList<>();
            // 如果JsonNode是对象
            ObjectNode objectNode = (ObjectNode) node;
            // 待移除的字段列表初始化,本列表的作用是暂存所有满足条件待移除的字段
            List<String> toRemoveFieldList = new ArrayList<>();
            Iterator<Map.Entry<String, JsonNode>> nodeFieldList = objectNode.fields();
            while (nodeFieldList.hasNext()) {
                Map.Entry<String, JsonNode> nodeField = nodeFieldList.next();
                String fieldName = nodeField.getKey();
                JsonNode fieldValue = nodeField.getValue();
                // 如果当前字段名的前缀属于待移除前缀列表中的任何一个元素,说明当前这个字段需要移除,加入到toRemoveFieldList
                for (String toRemoveFieldPrefix : toRemoveFieldPrefixList) {
                    if (fieldName.startsWith(toRemoveFieldPrefix.toLowerCase())) {
                       toRemoveFieldList.add(fieldName);
                    }
                }
                // 继续判断字段值是不是一个对象,如果是则递归调用当前方法进行处理;如果是数组则循环递归调用当前方法进行处理
                if (fieldValue.isObject()) {
                    responseDataParseAndRemove(fieldValue, toRemoveFieldPrefixList, language);
                } else if (fieldValue.isArray()) {
                    ArrayNode arrayNode = (ArrayNode) fieldValue;
                    for (JsonNode element : arrayNode) {
                      responseDataParseAndRemove(element, toRemoveFieldPrefixList, language);
                    }
                }
                // 需要将当前语言的值替换到目标字段,所以维护一个含有当前语言前缀的字段和目标字段的
                if (fieldName.startsWith(language.toLowerCase())) {
                    currentLanguageFieldNameList.add(fieldName);
                 }
            }
            // 一次性移除所有待移除的字段
            objectNode.remove(toRemoveFieldList);
            // 一次性替换所有字段
            for (String fieldName : currentLanguageFieldNameList) {
                // 新建一个目标字段:规则为原字段去掉当前语言前缀再转换为小驼峰,比如当前语言是cn,原字段是cnName,去掉前缀后就是再将首字母转换为小写就可以得到name
                String targetFiledName = fieldName.substring(language.length());
                targetFiledName = targetFiledName.substring(0, 1).toLowerCase() + targetFiledName.substring(1);
                // 获取到原字段的值
                JsonNode fieldValue = objectNode.get(fieldName);
                // 将目标字段添加到树模型中,原字段的值作为目标字段的值
                objectNode.set(targetFiledName, fieldValue);
                // 移除原字段
                objectNode.remove(fieldName);
            }
        } else if (node.isArray()) {
            // 如果JsonNode是数组
            ArrayNode arrayNode = (ArrayNode) node;
            for (JsonNode element : arrayNode) {
                responseDataParseAndRemove(element, toRemoveFieldPrefixList, language);
            }
        }
   } else {
        // 非容器节点
        log.info("非容器节点,不需要任何处理");
        }
   }

测试效果看一看:

 这样就完成了动态数据的国际化处理,根据不同的语言前缀返回对应语言的数据。其核心思想是在数据表中为不同语言建立相应的字段,然后通过语言前缀对响应数据进行处理。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐