1. 概述- 对于 yaml yml json xml 数据类型的 Namespace ,仅有一条 Item 记录,所以批量修改实际是修改该条 Item 。
- 对于 properties 数据类型的 Namespace ,有多条 Item 记录,所以批量变更是多条 Item 。
整体流程如下图: 老艿艿:因为 Portal 是管理后台,所以从代码实现上,和业务系统非常相像。也因此,本文会略显啰嗦。 2. ItemChangeSetscom.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 FileTextResolvercom.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.2 PropertyResolvercom.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();}第 19 至 44 行:创建 ItemChangeSets 对象,并解析配置文本到 ItemChangeSets 中。 - 第 23 行:循环 newItems 。
- 第 27 行:使用行号,获得对应的老的 ItemDTO 配置项。
- ========== 注释配置项 【基于行数】 ==========
第 29 行:调用 #isCommentItem(newItem) 方法,判断是否为注释配置文本。代码如下: private boolean isCommentItem(String line) { return line != null && (line.startsWith("#") || line.startsWith("!"));}第 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: }========== 空行配置项 【基于行数】 ========== 第 32 行:调用 调用 #isBlankItem(newItem) 方法,判断是否为空行配置文本。代码如下: private boolean isBlankItem(String line) { return "".equals(line);}第 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: }========== 普通配置项 【基于 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: }
第 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 的删除项。
|