A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

1. 概述
老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》
本文接 《Apollo 源码解析 —— Portal 创建 Item》 文章,分享 Item 的批量变更。
  • 对于 yaml yml json xml 数据类型的 Namespace ,仅有一条 Item 记录,所以批量修改实际是修改该条 Item 。
  • 对于 properties 数据类型的 Namespace ,有多条 Item 记录,所以批量变更是多条 Item 。
整体流程如下图:
老艿艿:因为 Portal 是管理后台,所以从代码实现上,和业务系统非常相像。也因此,本文会略显啰嗦。
2. ItemChangeSets
com.ctrip.framework.apollo.common.dto.ItemChangeSets ,Item 变更集合。代码如下:
public class ItemChangeSets extends BaseDTO {    /**     * 新增 Item 集合     */    private List<ItemDTO> createItems = new LinkedList<>();    /**     * 修改 Item 集合     */    private List<ItemDTO> updateItems = new LinkedList<>();    /**     * 删除 Item 集合     */    private List<ItemDTO> deleteItems = new LinkedList<>();    public void addCreateItem(ItemDTO item) {        createItems.add(item);    }    public void addUpdateItem(ItemDTO item) {        updateItems.add(item);    }    public void addDeleteItem(ItemDTO item) {        deleteItems.add(item);    }        public boolean isEmpty() {        return createItems.isEmpty() && updateItems.isEmpty() && deleteItems.isEmpty();    }        // ... 省略 setting / getting 方法}3. ConfigTextResolver
在 apollo-portal 项目中,com.ctrip.framework.apollo.portal.component.txtresolver.ConfigTextResolver ,配置文本解析器接口。代码如下:
public interface ConfigTextResolver {    /**     * 解析文本,创建 ItemChangeSets 对象     *     * @param namespaceId Namespace 编号     * @param configText 配置文本     * @param baseItems 已存在的 ItemDTO 们     * @return ItemChangeSets 对象     */    ItemChangeSets resolve(long namespaceId, String configText, List<ItemDTO> baseItems);}3.1 FileTextResolver
com.ctrip.framework.apollo.portal.component.txtresolver.FileTextResolver ,实现 ConfigTextResolver 接口,文件配置文本解析器,适用于 yaml、yml、json、xml 格式。代码如下:
1: @Override 2: public ItemChangeSets resolve(long namespaceId, String configText, List<ItemDTO> baseItems) { 3:     ItemChangeSets changeSets = new ItemChangeSets(); 4:     // 配置文本为空,不进行修改 5:     if (StringUtils.isEmpty(configText)) { 6:         return changeSets; 7:     } 8:     // 不存在已有配置,创建 ItemDTO 到 ItemChangeSets 新增项 9:     if (CollectionUtils.isEmpty(baseItems)) {10:         changeSets.addCreateItem(createItem(namespaceId, 0, configText));11:     // 已存在配置,创建 ItemDTO 到 ItemChangeSets 修改项12:     } else {13:         ItemDTO beforeItem = baseItems.get(0);14:         if (!configText.equals(beforeItem.getValue())) { //update15:             changeSets.addUpdateItem(createItem(namespaceId, beforeItem.getId(), configText));16:         }17:     }18:     return changeSets;19: }
  • 第 3 行:创建 ItemChangeSets 对象。
  • 第 4 至 7 行:若配置文件为空,不进行修改。
  • 第 8 至 10 行:不存在已有配置( baseItems ) ,创建 ItemDTO 到 ItemChangeSets 新增项。
  • 第 11 至 17 行:已存在配置,并且配置值不相等,创建 ItemDTO 到 ItemChangeSets 修改项。注意,选择了第一条 ItemDTO 进行对比,因为 yaml 等,有且仅有一条。
  • #createItem(long namespaceId, long itemId, String value) 方法,创建 ItemDTO 对象。代码如下:
    private ItemDTO createItem(long namespaceId, long itemId, String value) {    ItemDTO item = new ItemDTO();    item.setId(itemId);    item.setNamespaceId(namespaceId);    item.setValue(value);    item.setLineNum(1);    item.setKey(ConfigConsts.CONFIG_FILE_CONTENT_KEY);    return item;}
3.2 PropertyResolver
com.ctrip.framework.apollo.portal.component.txtresolver.PropertyResolver ,实现 ConfigTextResolver 接口,properties 配置解析器。代码如下:
1: private static final String KV_SEPARATOR = "="; 2: private static final String ITEM_SEPARATOR = "\n"; 3:  4: @Override 5: public ItemChangeSets resolve(long namespaceId, String configText, List<ItemDTO> baseItems) { 6:     // 创建 Item Map ,以 lineNum 为 键 7:     Map<Integer, ItemDTO> oldLineNumMapItem = BeanUtils.mapByKey("lineNum", baseItems); 8:     // 创建 Item Map ,以 key 为 键 9:     Map<String, ItemDTO> oldKeyMapItem = BeanUtils.mapByKey("key", baseItems);10:     oldKeyMapItem.remove(""); // remove comment and blank item map.11: 12:     // 按照拆分 Property 配置13:     String[] newItems = configText.split(ITEM_SEPARATOR);14:     // 校验是否存在重复配置 Key 。若是,抛出 BadRequestException 异常15:     if (isHasRepeatKey(newItems)) {16:         throw new BadRequestException("config text has repeat key please check.");17:     }18: 19:     // 创建 ItemChangeSets 对象,并解析配置文件到 ItemChangeSets 中。20:     ItemChangeSets changeSets = new ItemChangeSets();21:     Map<Integer, String> newLineNumMapItem = new HashMap<>();//use for delete blank and comment item22:     int lineCounter = 1;23:     for (String newItem : newItems) {24:         newItem = newItem.trim();25:         newLineNumMapItem.put(lineCounter, newItem);26:         // 使用行号,获得已存在的 ItemDTO27:         ItemDTO oldItemByLine = oldLineNumMapItem.get(lineCounter);28:         // comment item 注释 Item29:         if (isCommentItem(newItem)) {30:             handleCommentLine(namespaceId, oldItemByLine, newItem, lineCounter, changeSets);31:         // blank item 空白 Item32:         } else if (isBlankItem(newItem)) {33:             handleBlankLine(namespaceId, oldItemByLine, lineCounter, changeSets);34:         // normal item 普通 Item35:         } else {36:             handleNormalLine(namespaceId, oldKeyMapItem, newItem, lineCounter, changeSets);37:         }38:         // 行号计数 + 139:         lineCounter++;40:     }41:     // 删除注释和空行配置项42:     deleteCommentAndBlankItem(oldLineNumMapItem, newLineNumMapItem, changeSets);43:     // 删除普通配置项44:     deleteNormalKVItem(oldKeyMapItem, changeSets);45:     return changeSets;46: }
  • 第 7 行:调用 BeanUtils#mapByKey(String key, List<? extends Object> list) 方法,创建 ItemDTO Map oldLineNumMapItem ,以 lineNum 属性为键。
  • 第 9 至 10 行:调用 BeanUtils#mapByKey(String key, List<? extends Object> list) 方法,创建 ItemDTO Map oldKeyMapItem ,以 key 属性为键。
    • 移除 key ="" 的原因是,移除注释和空行的配置项。
  • 第 13 行:按照 "\n" 拆分 properties 配置。
  • 第 15 至 17 行:调用 #isHasRepeatKey(newItems) 方法,校验是否存在重复配置 Key 。若是,抛出 BadRequestException 异常。代码如下:
    private boolean isHasRepeatKey(String[] newItems) {    Set<String> keys = new HashSet<>();    int lineCounter = 1; // 记录行数,用于报错提示,无业务逻辑需要。    int keyCount = 0; // 计数    for (String item : newItems) {        if (!isCommentItem(item) && !isBlankItem(item)) { // 排除注释和空行的配置项            keyCount++;            String[] kv = parseKeyValueFromItem(item);            if (kv != null) {                keys.add(kv[0]);            } else {                throw new BadRequestException("line:" + lineCounter + " key value must separate by '='");            }        }        lineCounter++;    }    return keyCount > keys.size();}
    • 基于 Set 做排重判断。
  • 第 19 至 44 行:创建 ItemChangeSets 对象,并解析配置文本到 ItemChangeSets 中。
    • 第 23 行:循环 newItems 。
    • 第 27 行:使用行号,获得对应的老的 ItemDTO 配置项。
    • ========== 注释配置项 【基于行数】 ==========
    • 第 29 行:调用 #isCommentItem(newItem) 方法,判断是否为注释配置文本。代码如下:
      private boolean isCommentItem(String line) {    return line != null && (line.startsWith("#") || line.startsWith("!"));}
      • x
    • 第 30 行:调用 #handleCommentLine(namespaceId, oldItemByLine, newItem, lineCounter, changeSets) 方法,处理注释配置项。代码如下:
      1: private void handleCommentLine(Long namespaceId, ItemDTO oldItemByLine, String newItem, int lineCounter, ItemChangeSets changeSets) {2:     String oldComment = oldItemByLine == null ? "" : oldItemByLine.getComment();3:     // create comment. implement update comment by delete old comment and create new comment4:     // 创建注释 ItemDTO 到 ItemChangeSets 的新增项,若老的配置项不是注释或者不相等。另外,更新注释配置,通过删除 + 添加的方式。5:     if (!(isCommentItem(oldItemByLine) && newItem.equals(oldComment))) {6:         changeSets.addCreateItem(buildCommentItem(0L, namespaceId, newItem, lineCounter));7:     }8: }
      • 创建注释 ItemDTO 到 ItemChangeSets 的新增项,若老的配置项不是注释或者不相等。另外,更新注释配置,通过删除 + 添加的方式。
      • #buildCommentItem(id, namespaceId, comment, lineNum) 方法,创建注释 ItemDTO 对象。代码如下:
        private ItemDTO buildCommentItem(Long id, Long namespaceId, String comment, int lineNum) {    return buildNormalItem(id, namespaceId, ""/* key */, "" /* value */, comment, lineNum);}
        • key 和 value 的属性,使用 "" 空串。

    • ========== 空行配置项 【基于行数】 ==========
    • 第 32 行:调用 调用 #isBlankItem(newItem) 方法,判断是否为空行配置文本。代码如下:
      private boolean isBlankItem(String line) {    return "".equals(line);}
      • x
    • 第 33 行:调用 #handleBlankLine(namespaceId, oldItemByLine, lineCounter, changeSets) 方法,处理空行配置项。代码如下:
      1: private void handleBlankLine(Long namespaceId, ItemDTO oldItem, int lineCounter, ItemChangeSets changeSets) {2:     // 创建空行 ItemDTO 到 ItemChangeSets 的新增项,若老的不是空行。另外,更新空行配置,通过删除 + 添加的方式3:     if (!isBlankItem(oldItem)) {4:         changeSets.addCreateItem(buildBlankItem(0L, namespaceId, lineCounter));5:     }6: }
      • 创建空行 ItemDTO 到 ItemChangeSets 的新增项,若老的不是空行。另外,更新空行配置,通过删除 + 添加的方式。
      • #buildBlankItem(id, namespaceId, lineNum) 方法,处理空行配置项。代码如下:
        private ItemDTO buildBlankItem(Long id, Long namespaceId, int lineNum) {    return buildNormalItem(id, namespaceId, "" /* key */, "" /* value */, "" /* comment */, lineNum);}
        • 和 #buildCommentItem(...) 的差异点是,comment 是 "" 空串。

    • ========== 普通配置项 【基于 Key 】 ==========
    • 第 36 行:调用 #handleNormalLine(namespaceId, oldKeyMapItem, newItem, lineCounter, changeSets) 方法,处理普通配置项。代码如下:
      1: private void handleNormalLine(Long namespaceId, Map<String, ItemDTO> keyMapOldItem, String newItem, 2:                               int lineCounter, ItemChangeSets changeSets) { 3:     // 解析一行,生成 [key, value] 4:     String[] kv = parseKeyValueFromItem(newItem); 5:     if (kv == null) { 6:         throw new BadRequestException("line:" + lineCounter + " key value must separate by '='"); 7:     } 8:     String newKey = kv[0]; 9:     String newValue = kv[1].replace("\\n", "\n"); //handle user input \n10:     // 获得老的 ItemDTO 对象11:     ItemDTO oldItem = keyMapOldItem.get(newKey);12:     // 不存在,则创建 ItemDTO 到 ItemChangeSets 的添加项13:     if (oldItem == null) {//new item14:         changeSets.addCreateItem(buildNormalItem(0L, namespaceId, newKey, newValue, "", lineCounter));15:     // 如果值或者行号不相等,则创建 ItemDTO 到 ItemChangeSets 的修改项16:     } else if (!newValue.equals(oldItem.getValue()) || lineCounter != oldItem.getLineNum()) {//update item17:         changeSets.addUpdateItem(buildNormalItem(oldItem.getId(), namespaceId, newKey, newValue, oldItem.getComment(), lineCounter));18:     }19:     // 移除老的 ItemDTO 对象20:     keyMapOldItem.remove(newKey);21: }
      • 第 3 至 9 行:调用 #parseKeyValueFromItem(newItem) 方法,解析一行,生成 [key, value] 。代码如下:
        private String[] parseKeyValueFromItem(String item) {    int kvSeparator = item.indexOf(KV_SEPARATOR);    if (kvSeparator == -1) {        return null;    }    String[] kv = new String[2];    kv[0] = item.substring(0, kvSeparator).trim();    kv[1] = item.substring(kvSeparator + 1, item.length()).trim();    return kv;}
        • x
      • 第 11 行:获得老的 ItemDTO 对象。
      • 第 12 至 14 行:若老的 Item DTO 对象不存在,则创建 ItemDTO 到 ItemChangeSets 的新增项。
      • 第 15 至 18 行:若老的 Item DTO 对象存在,且或者行数不相等,则创建 ItemDTO 到 ItemChangeSets 的修改项。
      • 第 20 行:移除老的 ItemDTO 对象。这样,最终 keyMapOldItem 保留的是,需要删除的普通配置项,详细见 #deleteNormalKVItem(oldKeyMapItem, changeSets) 方法。

  • 第 42 行:调用 #deleteCommentAndBlankItem(oldLineNumMapItem, newLineNumMapItem, changeSets) 方法,删除注释和空行配置项。代码如下:
    private void deleteCommentAndBlankItem(Map<Integer, ItemDTO> oldLineNumMapItem,                                       Map<Integer, String> newLineNumMapItem,                                       ItemChangeSets changeSets) {    for (Map.Entry<Integer, ItemDTO> entry : oldLineNumMapItem.entrySet()) {        int lineNum = entry.getKey();        ItemDTO oldItem = entry.getValue();        String newItem = newLineNumMapItem.get(lineNum);        // 添加到 ItemChangeSets 的删除项        // 1. old is blank by now is not        // 2. old is comment by now is not exist or modified        if ((isBlankItem(oldItem) && !isBlankItem(newItem)) // 老的是空行配置项,新的不是空行配置项                || isCommentItem(oldItem) && (newItem == null || !newItem.equals(oldItem.getComment()))) { // 老的是注释配置项,新的不相等            changeSets.addDeleteItem(oldItem);        }    }}
    • 将需要删除( 具体条件看注释 ) 的注释和空白配置项,添加到 ItemChangeSets 的删除项中。
  • 第 44 行:调用 #deleteNormalKVItem(oldKeyMapItem, changeSets) 方法,删除普通配置项。代码如下:
    private void deleteNormalKVItem(Map<String, ItemDTO> baseKeyMapItem, ItemChangeSets changeSets) {    // 将剩余的配置项,添加到 ItemChangeSets 的删除项    // surplus item is to be deleted    for (Map.Entry<String, ItemDTO> entry : baseKeyMapItem.entrySet()) {        changeSets.addDeleteItem(entry.getValue());    }}
    • 将剩余的配置项( oldLineNumMapItem ),添加到 ItemChangeSets 的删除项。

3 个回复

倒序浏览
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马