⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Apollo/portal-update-item-set/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

阅读源码最好的方式,是使用 IDEA 进行调试 Apollo 源码,不然会一脸懵逼。

胖友可以点击「芋道源码」扫码关注,回复 git018 关键字
获得艿艿添加了中文注释的 Apollo 源码地址。

阅读源码很孤单,加入源码交流群,一起坚持!

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 接口,文件配置文本解析器,适用于 yamlymljsonxml 格式。代码如下:

 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())) { //update
15: 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 item
22: int lineCounter = 1;
23: for (String newItem : newItems) {
24: newItem = newItem.trim();
25: newLineNumMapItem.put(lineCounter, newItem);
26: // 使用行号,获得已存在的 ItemDTO
27: ItemDTO oldItemByLine = oldLineNumMapItem.get(lineCounter);
28: // comment item 注释 Item
29: if (isCommentItem(newItem)) {
30: handleCommentLine(namespaceId, oldItemByLine, newItem, lineCounter, changeSets);
31: // blank item 空白 Item
32: } else if (isBlankItem(newItem)) {
33: handleBlankLine(namespaceId, oldItemByLine, lineCounter, changeSets);
34: // normal item 普通 Item
35: } else {
36: handleNormalLine(namespaceId, oldKeyMapItem, newItem, lineCounter, changeSets);
37: }
38: // 行号计数 + 1
39: 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 comment
      4: // 创建注释 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);
        }

        • keyvalue 的属性,使用 "" 空串。
    • ========== 空行配置项 【基于行数】 ==========

    • 第 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 \n
      10: // 获得老的 ItemDTO 对象
      11: ItemDTO oldItem = keyMapOldItem.get(newKey);
      12: // 不存在,则创建 ItemDTO 到 ItemChangeSets 的添加项
      13: if (oldItem == null) {//new item
      14: changeSets.addCreateItem(buildNormalItem(0L, namespaceId, newKey, newValue, "", lineCounter));
      15: // 如果值或者行号不相等,则创建 ItemDTO 到 ItemChangeSets 的修改项
      16: } else if (!newValue.equals(oldItem.getValue()) || lineCounter != oldItem.getLineNum()) {//update item
      17: 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 的删除项

🙂 整个方法比较冗长,建议胖友多多调试,有几个点特别需要注意:

  • 对于注释空行配置项,基于行数做比较。当发生变化时,使用删除 + 创建的方式。笔者的理解是,注释和空行配置项,是没有 Key ,每次变化都认为是新的。另外,这样也可以和注释空行配置项被改成普通配置项,保持一致。例如,第一行原先是注释配置项,改成了普通配置项,从数据上也是删除 + 创建的方式。
  • 对于普通配置项,基于 Key 做比较。例如,第一行原先是普通配置项,结果我们在敲了回车,在第一行添加了注释,那么认为是普通配置项修改了行数

4. Portal 侧

4.1 ItemController

apollo-portal 项目中,com.ctrip.framework.apollo.portal.controller.ItemController ,提供 Item 的 API

在【批量变更 Namespace 配置项】的界面中,点击【 √ 】按钮,调用批量变更 Namespace 的 Item 们的 API

批量变更 Namespace 配置项

#modifyItemsByText(appId, env, clusterName, namespaceName, NamespaceTextModel) 方法,批量变更 Namespace 的 Item 们。代码如下:

 1: @Autowired
2: private ItemService configService;
3:
4: @PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName)")
5: @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items", method = RequestMethod.PUT, consumes = {"application/json"})
6: public void modifyItemsByText(@PathVariable String appId, @PathVariable String env,
7: @PathVariable String clusterName, @PathVariable String namespaceName,
8: @RequestBody NamespaceTextModel model) {
9: // 校验 `model` 非空
10: checkModel(model != null);
11: // 设置 PathVariable 到 `model` 中
12: model.setAppId(appId);
13: model.setClusterName(clusterName);
14: model.setEnv(env);
15: model.setNamespaceName(namespaceName);
16: // 批量更新一个 Namespace 下的 Item 们
17: configService.updateConfigItemByText(model);
18: }

  • POST /apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items 接口,Request Body 传递 JSON 对象。

  • @PreAuthorize(...) 注解,调用 PermissionValidator#hasModifyNamespacePermission(appId, namespaceName) 方法,校验是否有修改 Namespace 的权限。后续文章,详细分享。

  • com.ctrip.framework.apollo.portal.entity.model.NamespaceTextModel ,Namespace 下的配置文本 Model 。代码如下:

    public class NamespaceTextModel implements Verifiable {

    /**
    * App 编号
    */
    private String appId;
    /**
    * Env 名
    */
    private String env;
    /**
    * Cluster 名
    */
    private String clusterName;
    /**
    * Namespace 名
    */
    private String namespaceName;
    /**
    * Namespace 编号
    */
    private int namespaceId;
    /**
    * 格式
    */
    private String format;
    /**
    * 配置文本
    */
    private String configText;

    @Override
    public boolean isInvalid() {
    return StringUtils.isContainEmpty(appId, env, clusterName, namespaceName) || namespaceId <= 0;
    }
    }

    • 重点是 configText 属性,配置文本。
  • 第 10 行:校验 NamespaceTextModel 非空。

  • 第 11 至 15 行:设置 PathVariable 变量,到 NamespaceTextModel 中 。

  • 第 17 行:调用 ItemService#updateConfigItemByText(NamespaceTextModel) 方法,批量更新一个 Namespace 下的 Item

4.2 ItemService

apollo-portal 项目中,com.ctrip.framework.apollo.portal.service.ItemService ,提供 Item 的 Service 逻辑。

#updateConfigItemByText(NamespaceTextModel) 方法,解析配置文本,并批量更新 Namespace 的 Item 们。代码如下:

 1: @Autowired
2: private UserInfoHolder userInfoHolder;
3: @Autowired
4: private AdminServiceAPI.ItemAPI itemAPI;
5:
6: @Autowired
7: @Qualifier("fileTextResolver")
8: private ConfigTextResolver fileTextResolver;
9: @Autowired
10: @Qualifier("propertyResolver")
11: private ConfigTextResolver propertyResolver;
12:
13: public void updateConfigItemByText(NamespaceTextModel model) {
14: String appId = model.getAppId();
15: Env env = model.getEnv();
16: String clusterName = model.getClusterName();
17: String namespaceName = model.getNamespaceName();
18: long namespaceId = model.getNamespaceId();
19: String configText = model.getConfigText();
20: // 获得对应格式的 ConfigTextResolver 对象
21: ConfigTextResolver resolver = model.getFormat() == ConfigFileFormat.Properties ? propertyResolver : fileTextResolver;
22: // 解析成 ItemChangeSets
23: ItemChangeSets changeSets = resolver.resolve(namespaceId, configText, itemAPI.findItems(appId, env, clusterName, namespaceName));
24: if (changeSets.isEmpty()) {
25: return;
26: }
27: // 设置修改人为当前管理员
28: changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
29: // 调用 Admin Service API ,批量更新 Item 们。
30: updateItems(appId, env, clusterName, namespaceName, changeSets);
31: // 【TODO 6001】Tracer 日志
32: Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE_BY_TEXT, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
33: Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
34: }

  • 第 21 行:获得对应格式( format )的 ConfigTextResolver 对象。

  • 第 23 行:调用 ItemAPI#findItems(appId, env, clusterName, namespaceName) 方法,获得 Namespace 下所有的 ItemDTO 配置项们。

  • 第 23 行:调用 ConfigTextResolver#resolve(...) 方法,解析配置文本,生成 ItemChangeSets 对象。

  • 第 24 至 26 行:调用 ItemChangeSets#isEmpty() 方法,若无变更项,直接返回。

  • 第 30 行:调用 #updateItems(appId, env, clusterName, namespaceName, changeSets) 方法,调用 Admin Service API ,批量更新 Namespace 下的 Item 们。代码如下:

    public void updateItems(String appId, Env env, String clusterName, String namespaceName, ItemChangeSets changeSets) {
    itemAPI.updateItemsByChangeSet(appId, env, clusterName, namespaceName, changeSets);
    }

  • 第 31 至 33 行:【TODO 6001】Tracer 日志

4.3 ItemAPI

com.ctrip.framework.apollo.portal.api.ItemAPI ,实现 API 抽象类,封装对 Admin Service 的 Item 模块的 API 调用。代码如下:

ItemAPI

5. Admin Service 侧

5.1 ItemSetController

apollo-adminservice 项目中, com.ctrip.framework.apollo.adminservice.controller.ItemSetController ,提供 Item 批量API

#create(appId, clusterName, namespaceName, ItemChangeSets) 方法,批量更新 Namespace 下的 Item 们。代码如下:

@RestController
public class ItemSetController {

@Autowired
private ItemSetService itemSetService;

@PreAcquireNamespaceLock
@RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset", method = RequestMethod.POST)
public ResponseEntity<Void> create(@PathVariable String appId, @PathVariable String clusterName,
@PathVariable String namespaceName, @RequestBody ItemChangeSets changeSet) {
// 批量更新 Namespace 下的 Item 们
itemSetService.updateSet(appId, clusterName, namespaceName, changeSet);
return ResponseEntity.status(HttpStatus.OK).build();
}

}

  • POST /apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/itemset 接口,Request Body 传递 JSON 对象。

5.2 ItemSetService

apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.ItemSetService ,提供 Item 批量Service 逻辑给 Admin Service 和 Config Service 。

#updateSet(Namespace, ItemChangeSets) 方法,批量更新 Namespace 下的 Item 们。代码如下:

 1: @Service
2: public class ItemSetService {
3:
4: @Autowired
5: private AuditService auditService;
6: @Autowired
7: private CommitService commitService;
8: @Autowired
9: private ItemService itemService;
10:
11: @Transactional
12: public ItemChangeSets updateSet(Namespace namespace, ItemChangeSets changeSets) {
13: return updateSet(namespace.getAppId(), namespace.getClusterName(), namespace.getNamespaceName(), changeSets);
14: }
15:
16: @Transactional
17: public ItemChangeSets updateSet(String appId, String clusterName,
18: String namespaceName, ItemChangeSets changeSet) {
19: String operator = changeSet.getDataChangeLastModifiedBy();
20: ConfigChangeContentBuilder configChangeContentBuilder = new ConfigChangeContentBuilder();
21: // 保存 Item 们
22: if (!CollectionUtils.isEmpty(changeSet.getCreateItems())) {
23: for (ItemDTO item : changeSet.getCreateItems()) {
24: Item entity = BeanUtils.transfrom(Item.class, item);
25: entity.setDataChangeCreatedBy(operator);
26: entity.setDataChangeLastModifiedBy(operator);
27: // 保存 Item
28: Item createdItem = itemService.save(entity);
29: // 添加到 ConfigChangeContentBuilder 中
30: configChangeContentBuilder.createItem(createdItem);
31: }
32: // 记录 Audit 到数据库中
33: auditService.audit("ItemSet", null, Audit.OP.INSERT, operator);
34: }
35: // 更新 Item 们
36: if (!CollectionUtils.isEmpty(changeSet.getUpdateItems())) {
37: for (ItemDTO item : changeSet.getUpdateItems()) {
38: Item entity = BeanUtils.transfrom(Item.class, item);
39: Item managedItem = itemService.findOne(entity.getId());
40: if (managedItem == null) {
41: throw new NotFoundException(String.format("item not found.(key=%s)", entity.getKey()));
42: }
43: Item beforeUpdateItem = BeanUtils.transfrom(Item.class, managedItem);
44: // protect. only value,comment,lastModifiedBy,lineNum can be modified
45: managedItem.setValue(entity.getValue());
46: managedItem.setComment(entity.getComment());
47: managedItem.setLineNum(entity.getLineNum());
48: managedItem.setDataChangeLastModifiedBy(operator);
49: // 更新 Item
50: Item updatedItem = itemService.update(managedItem);
51: // 添加到 ConfigChangeContentBuilder 中
52: configChangeContentBuilder.updateItem(beforeUpdateItem, updatedItem);
53: }
54: // 记录 Audit 到数据库中
55: auditService.audit("ItemSet", null, Audit.OP.UPDATE, operator);
56: }
57: // 删除 Item 们
58: if (!CollectionUtils.isEmpty(changeSet.getDeleteItems())) {
59: for (ItemDTO item : changeSet.getDeleteItems()) {
60: // 删除 Item
61: Item deletedItem = itemService.delete(item.getId(), operator);
62: // 添加到 ConfigChangeContentBuilder 中
63: configChangeContentBuilder.deleteItem(deletedItem);
64: }
65: // 记录 Audit 到数据库中
66: auditService.audit("ItemSet", null, Audit.OP.DELETE, operator);
67: }
68: // 创建 Commit 对象,并保存
69: if (configChangeContentBuilder.hasContent()) {
70: createCommit(appId, clusterName, namespaceName, configChangeContentBuilder.build(), changeSet.getDataChangeLastModifiedBy());
71: }
72: return changeSet;
73:
74: }
75:
76: private void createCommit(String appId, String clusterName, String namespaceName, String configChangeContent,
77: String operator) {
78: // 创建 Commit 对象
79: Commit commit = new Commit();
80: commit.setAppId(appId);
81: commit.setClusterName(clusterName);
82: commit.setNamespaceName(namespaceName);
83: commit.setChangeSets(configChangeContent);
84: commit.setDataChangeCreatedBy(operator);
85: commit.setDataChangeLastModifiedBy(operator);
86: // 保存 Commit 对象
87: commitService.save(commit);
88: }
89:
90: }

  • 第 21 至 34 行:保存 Item 们。
  • 第 35 至 56 行:更新 Item 们。
    • 第 40 至 42 行:若更新的 Item 不存在,抛出 NotFoundException 异常,事务回滚
  • 第 57 至 67 行:删除 Item 们。
    • 第 61 行:在 ItemService#delete(long id, String operator) 方法中,会校验删除的 Item 是否存在。若不存在,会抛出 IllegalArgumentException 异常,事务回滚
  • 第 69 至 71 行:调用 ConfigChangeContentBuilder#hasContent() 方法,判断若有变更,则调用 #createCommit(appId, clusterName, namespaceName, configChangeContent, operator) 方法,创建并保存 Commit 。

666. 彩蛋

ConfigTextResolver 的设计,值得我们在业务系统开发学习。🙂 很多时候,我们习惯性把大量的逻辑,全部写在 Service 类中。

知识星球

文章目录
  1. 1. 1. 概述
  2. 2. 2. ItemChangeSets
  3. 3. 3. ConfigTextResolver
    1. 3.1. 3.1 FileTextResolver
    2. 3.2. 3.2 PropertyResolver
  4. 4. 4. Portal 侧
    1. 4.1. 4.1 ItemController
    2. 4.2. 4.2 ItemService
    3. 4.3. 4.3 ItemAPI
  5. 5. 5. Admin Service 侧
    1. 5.1. 5.1 ItemSetController
    2. 5.2. 5.2 ItemSetService
  6. 6. 666. 彩蛋