Apollo 源碼解析 —— Portal 創建 Item

點擊上方“芋道源碼”,選擇“設爲星標

做積極的人,而不是積極廢人!

源碼精品專欄

 

摘要: 原創出處 http://www.iocoder.cn/Apollo/portal-create-item/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

  • 1. 概述

  • 2. 實體

  • 3. Portal 側

  • 4. Admin Service 側

  • 666. 彩蛋


1. 概述

老艿艿:本系列假定胖友已經閱讀過 《Apollo 官方 wiki 文檔》 。

Item ,配置項,是 Namespace 下最小顆粒度的單位。在 Namespace 分成五種類型:properties yml yaml json xml 。其中:

  • properties每一行配置對應一條 Item 記錄。

  • 後四者:無法進行拆分,所以一個 Namespace 僅僅對應一條 Item 記錄。

本文先分享 Portal 創建類型爲 properties 的 Namespace 的 Item 的流程,整個過程涉及 Portal、Admin Service ,如下圖所示:

流程

老艿艿:因爲 Portal 是管理後臺,所以從代碼實現上,和業務系統非常相像。也因此,本文會略顯囉嗦。

2. 實體

2.1 Item

com.ctrip.framework.apollo.biz.entity.Item ,繼承 BaseEntity 抽象類,Item 實體。代碼如下:

@Entity
@Table(name = "Item")
@SQLDelete(sql = "Update Item set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class Item extends BaseEntity {

    /**
     * Namespace 編號
     */
    @Column(name = "NamespaceId", nullable = false)
    private long namespaceId;
    /**
     * 鍵
     */
    @Column(name = "key", nullable = false)
    private String key;
    /**
     * 值
     */
    @Column(name = "value")
    @Lob
    private String value;
    /**
     * 註釋
     */
    @Column(name = "comment")
    private String comment;
    /**
     * 行號,從一開始。
     *
     * 例如 Properties 中,多個配置項。每個配置項對應一行。
     */
    @Column(name = "LineNum")
    private Integer lineNum;
}
  • namespaceId 字段,Namespace 編號,指向對應的 Namespace 記錄。

  • key 字段,鍵。

    • 對於  properties ,使用 Item 的 key ,對應每條配置項的鍵。

    • 對於 yaml 等等,使用 Item 的 key = content ,對應整個配置文件。

  • lineNum 字段,行號,從開始。主要用於 properties 類型的配置文件。

2.2 Commit

com.ctrip.framework.apollo.biz.entity.Commit ,繼承 BaseEntity 抽象類,Commit 實體,記錄 Item 的 KV 變更歷史。代碼如下:

@Entity
@Table(name = "Commit")
@SQLDelete(sql = "Update Commit set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class Commit extends BaseEntity {

    /**
     * App 編號
     */
    @Column(name = "AppId", nullable = false)
    private String appId;
    /**
     * Cluster 名字
     */
    @Column(name = "ClusterName", nullable = false)
    private String clusterName;
    /**
     * Namespace 名字
     */
    @Column(name = "NamespaceName", nullable = false)
    private String namespaceName;
    /**
     * 變更集合。
     *
     * JSON 格式化,使用 {@link com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder} 生成
     */
    @Lob
    @Column(name = "ChangeSets", nullable = false)
    private String changeSets;
    /**
     * 備註
     */
    @Column(name = "Comment")
    private String comment;
    
}
  • appId + clusterName + namespaceName  字段,可以確認唯一 Namespace 記錄。

  • changeSets 字段,Item 變更集合。JSON 格式化字符串,使用 ConfigChangeContentBuilder 構建。

2.2.1 ConfigChangeContentBuilder

com.ctrip.framework.apollo.biz.utils.ConfigChangeContentBuilder ,配置變更內容構建器。

構造方法

private static final Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();

/**
 * 創建 Item 集合
 */
private List<Item> createItems = new LinkedList<>();
/**
 * 更新 Item 集合
 */
private List<ItemPair> updateItems = new LinkedList<>();
/**
 * 刪除 Item 集合
 */
private List<Item> deleteItems = new LinkedList<>();
  • createItems 字段,添加代碼如下:

    public ConfigChangeContentBuilder createItem(Item item) {
        if (!StringUtils.isEmpty(item.getKey())) {
            createItems.add(cloneItem(item));
        }
        return this;
    }
    
    • x

    • 調用 #cloneItem(Item) 方法,克隆 Item 對象。因爲在 #build() 方法中,會修改 Item 對象的屬性。代碼如下:

      Item cloneItem(Item source) {
          Item target = new Item();
          BeanUtils.copyProperties(source, target);
          return target;
      }
      
  • updateItems 字段,添加代碼如下:

    public ConfigChangeContentBuilder updateItem(Item oldItem, Item newItem) {
        if (!oldItem.getValue().equals(newItem.getValue())) {
            ItemPair itemPair = new ItemPair(cloneItem(oldItem), cloneItem(newItem));
            updateItems.add(itemPair);
        }
        return this;
    }
    
    • x

    • ItemPair ,Item ,代碼如下:

      static class ItemPair {
      
          Item oldItem; // 老
          Item newItem; // 新
      
          public ItemPair(Item oldItem, Item newItem) {
              this.oldItem = oldItem;
              this.newItem = newItem;
          }
      
      }
      
  • deleteItems 字段,添加代碼如下:

    public ConfigChangeContentBuilder deleteItem(Item item) {
        if (!StringUtils.isEmpty(item.getKey())) {
            deleteItems.add(cloneItem(item));
        }
        return this;
    }
    

hasContent

#hasContent() 方法,判斷是否有變化。當且僅當有變化才記錄 Commit。代碼如下:

public boolean hasContent() {
    return !createItems.isEmpty() || !updateItems.isEmpty() || !deleteItems.isEmpty();
}

build

#build() 方法,構建 Item 變化的 JSON 字符串。代碼如下:

public String build() {
    // 因爲事務第一段提交並沒有更新時間,所以build時統一更新
    Date now = new Date();
    for (Item item : createItems) {
        item.setDataChangeLastModifiedTime(now);
    }
    for (ItemPair item : updateItems) {
        item.newItem.setDataChangeLastModifiedTime(now);
    }
    for (Item item : deleteItems) {
        item.setDataChangeLastModifiedTime(now);
    }
    // JSON 格式化成字符串
    return gson.toJson(this);
}
  • 例子如下:

    // 已經使用 http://tool.oschina.net/codeformat/json/ 進行格式化,實際是**緊湊型**
    {
        "createItems": [ ], 
        "updateItems": [
            {
                "oldItem": {
                    "namespaceId": 32, 
                    "key": "key4", 
                    "value": "value4123", 
                    "comment": "123", 
                    "lineNum": 4, 
                    "id": 15, 
                    "isDeleted": false, 
                    "dataChangeCreatedBy": "apollo", 
                    "dataChangeCreatedTime": "2018-04-27 16:49:59", 
                    "dataChangeLastModifiedBy": "apollo", 
                    "dataChangeLastModifiedTime": "2018-04-27 22:37:52"
                }, 
                "newItem": {
                    "namespaceId": 32, 
                    "key": "key4", 
                    "value": "value41234", 
                    "comment": "123", 
                    "lineNum": 4, 
                    "id": 15, 
                    "isDeleted": false, 
                    "dataChangeCreatedBy": "apollo", 
                    "dataChangeCreatedTime": "2018-04-27 16:49:59", 
                    "dataChangeLastModifiedBy": "apollo", 
                    "dataChangeLastModifiedTime": "2018-04-27 22:38:58"
                }
            }
        ], 
        "deleteItems": [ ]
    }
    

3. Portal 側

3.1 ItemController

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

在【添加配置項】的界面中,點擊【提交】按鈕,調用創建 Item 的 API

添加配置項

#createItem(appId, env, clusterName, namespaceName, ItemDTO) 方法,創建 Item 對象。代碼如下:

  1: @RestController
  2: public class ItemController {
  3: 
  4:     @Autowired
  5:     private ItemService configService;
  6:     @Autowired
  7:     private UserInfoHolder userInfoHolder;
  8:     
  9:     @PreAuthorize(value = "@permissionValidator.hasModifyNamespacePermission(#appId, #namespaceName)")
 10:     @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item", method = RequestMethod.POST)
 11:     public ItemDTO createItem(@PathVariable String appId, @PathVariable String env,
 12:                               @PathVariable String clusterName, @PathVariable String namespaceName,
 13:                               @RequestBody ItemDTO item) {
 14:         // 校驗 Item 格式正確
 15:         checkModel(isValidItem(item));
 16:         // protect
 17:         item.setLineNum(0);
 18:         item.setId(0);
 19:         // 設置 ItemDTO 的創建和修改人爲當前管理員
 20:         String userId = userInfoHolder.getUser().getUserId();
 21:         item.setDataChangeCreatedBy(userId);
 22:         item.setDataChangeLastModifiedBy(userId);
 23:         // protect
 24:         item.setDataChangeCreatedTime(null);
 25:         item.setDataChangeLastModifiedTime(null);
 26:         // 保存 Item 到 Admin Service
 27:         return configService.createItem(appId, Env.valueOf(env), clusterName, namespaceName, item);
 28:     }
 29:     
 30:     // ... 省略 deleteCluster 接口
 31: }    
  • POST /apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/item 接口,Request Body 傳遞 JSON 對象。

  • @PreAuthorize(...) 註解,調用 PermissionValidator#hasModifyNamespacePermission(appId, namespaceName) 方法,校驗是否有修改 Namespace 的權限。後續文章,詳細分享。

  • com.ctrip.framework.apollo.common.dto.ItemDTO ,Item DTO 。代碼如下:

    public class ItemDTO extends BaseDTO {
    
        /**
         * Item 編號
         */
        private long id;
        /**
         * Namespace 編號
         */
        private long namespaceId;
        /**
         * 鍵
         */
        private String key;
        /**
         * 值
         */
        private String value;
        /**
         * 備註
         */
        private String comment;
        /**
         * 行數
         */
        private int lineNum;
    }
    
  • 第 14 行:調用 #isValidItem(ItemDTO) 方法,校驗 Item 格式正確。代碼如下:

    private boolean isValidItem(ItemDTO item) {
        return Objects.nonNull(item) // 非空
                && !StringUtils.isContainEmpty(item.getKey()); // 鍵非空
    }
    
  • 第 16 至 18 行 && 第 23 至 25 行:防禦性編程,這幾個參數不需要從 Portal 傳遞。

  • 第 19 至 22 行:設置 ItemDTO 的創建和修改人爲當前管理員。

  • 第 27 行:調用 ConfigService#createItem(appId, Env, clusterName, namespaceName, ItemDTO) 方法,保存 Item 到 Admin Service 中。

3.2 ItemService

apollo-portal 項目中,com.ctrip.framework.apollo.portal.service.ItemService ,提供 Item 的 Service 邏輯。

#createItem(appId, env, clusterName, namespaceName, ItemDTO) 方法,創建並保存 Item 到 Admin Service 。代碼如下:

  1: @Autowired
  2: private AdminServiceAPI.NamespaceAPI namespaceAPI;
  3: @Autowired
  4: private AdminServiceAPI.ItemAPI itemAPI;
  5:     
  6: public ItemDTO createItem(String appId, Env env, String clusterName, String namespaceName, ItemDTO item) {
  7:     // 校驗 NamespaceDTO 是否存在。若不存在,拋出 BadRequestException 異常
  8:     NamespaceDTO namespace = namespaceAPI.loadNamespace(appId, env, clusterName, namespaceName);
  9:     if (namespace == null) {
 10:         throw new BadRequestException("namespace:" + namespaceName + " not exist in env:" + env + ", cluster:" + clusterName);
 11:     }
 12:     // 設置 ItemDTO 的 `namespaceId`
 13:     item.setNamespaceId(namespace.getId());
 14:     // 保存 Item 到 Admin Service
 15:     ItemDTO itemDTO = itemAPI.createItem(appId, env, clusterName, namespaceName, item);
 16:     // 【TODO 6001】Tracer 日誌
 17:     Tracer.logEvent(TracerEventType.MODIFY_NAMESPACE, String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));
 18:     return itemDTO;
 19: } 
  • 第 7 至11 行:調用 NamespaceAPI#loadNamespace(appId, Env, clusterName, namespaceName) 方法,校驗 Namespace 是否存在。若不存在,拋出 BadRequestException 異常。注意,此處是遠程調用 Admin Service 的 API 。

  • 第 12 行:設置 ItemDTO 的 namespaceId

  • 第 15 行:調用 NamespaceAPI#createItem(appId, Env, clusterName, namespaceName, ItemDTO) 方法,保存 Item 到 Admin Service 。

  • 第 17 行:【TODO 6001】Tracer 日誌

3.3 ItemAPI

com.ctrip.framework.apollo.portal.api.ItemAPI ,實現 API 抽象類,封裝對 Admin Service 的 Item 模塊的 API 調用。代碼如下:

ItemAPI

4. Admin Service 側

4.1 ItemController

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

#create(appId, clusterName, namespaceName, ItemDTO) 方法,創建 Item ,並記錄 Commit 。代碼如下:

  1: @RestController
  2: public class ItemController {
  3: 
  4:     @Autowired
  5:     private ItemService itemService;
  6:     @Autowired
  7:     private CommitService commitService;
  8: 
  9:     @PreAcquireNamespaceLock
 10:     @RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items", method = RequestMethod.POST)
 11:     public ItemDTO create(@PathVariable("appId") String appId,
 12:                           @PathVariable("clusterName") String clusterName,
 13:                           @PathVariable("namespaceName") String namespaceName,
 14:                           @RequestBody ItemDTO dto) {
 15:         // 將 ItemDTO 轉換成 Item 對象
 16:         Item entity = BeanUtils.transfrom(Item.class, dto);
 17:         // 創建 ConfigChangeContentBuilder 對象
 18:         ConfigChangeContentBuilder builder = new ConfigChangeContentBuilder();
 19:         // 校驗對應的 Item 是否已經存在。若是,拋出 BadRequestException 異常。
 20:         Item managedEntity = itemService.findOne(appId, clusterName, namespaceName, entity.getKey());
 21:         if (managedEntity != null) {
 22:             throw new BadRequestException("item already exist");
 23:         } else {
 24:             // 保存 Item 對象
 25:             entity = itemService.save(entity);
 26:             // 添加到 ConfigChangeContentBuilder 中
 27:             builder.createItem(entity);
 28:         }
 29:         // 將 Item 轉換成 ItemDTO 對象
 30:         dto = BeanUtils.transfrom(ItemDTO.class, entity);
 31:         // 創建 Commit 對象
 32:         Commit commit = new Commit();
 33:         commit.setAppId(appId);
 34:         commit.setClusterName(clusterName);
 35:         commit.setNamespaceName(namespaceName);
 36:         commit.setChangeSets(builder.build()); // ConfigChangeContentBuilder 構造變更
 37:         commit.setDataChangeCreatedBy(dto.getDataChangeLastModifiedBy());
 38:         commit.setDataChangeLastModifiedBy(dto.getDataChangeLastModifiedBy());
 39:         // 保存 Commit 對象
 40:         commitService.save(commit);
 41:         return dto;
 42:     }
 43:     
 44:     // ... 省略其他接口和屬性
 45: }
  • POST /apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items 接口,Request Body 傳遞 JSON 對象。

  • 第 16 行:調用 BeanUtils#transfrom(Class<T> clazz, Object src) 方法,將 ItemDTO 轉換成 Item 對象。

  • 第 18 行:創建 ConfigChangeContentBuilder 對象。

  • 第 19 至 22 行:調用 ItemService#findOne(appId, clusterName, namespaceName, key) 方法,校驗對應的 Item 是否已經存在。若是,拋出 BadRequestException 異常。

  • 第 25 行:調用 ItemService#save(Item) 方法,保存 Item 對象。

  • 第 27 行:調用 ConfigChangeContentBuilder#createItem(Item) 方法,添加到 ConfigChangeContentBuilder 中。

  • 第 30 行:調用 BeanUtils#transfrom(Class<T> clazz, Object src) 方法,將 Item 轉換成 ItemDTO 對象。

  • 第 31 至 38 行:創建 Commit 對象。

  • 第 40 行:調用 CommitService#save(Commit) 方法,保存 Commit 對象。

4.2 ItemService

apollo-biz 項目中,com.ctrip.framework.apollo.biz.service.ItemService ,提供 Item 的 Service 邏輯給 Admin Service 和 Config Service 。

#save(Item) 方法,保存 Item 對象 。代碼如下:

  1: @Autowired
  2: private ItemRepository itemRepository;
  3: @Autowired
  4: private AuditService auditService;
  5: 
  6: @Transactional
  7: public Item save(Item entity) {
  8:     // 校驗 Key 長度
  9:     checkItemKeyLength(entity.getKey());
 10:     // 校驗 Value 長度
 11:     checkItemValueLength(entity.getNamespaceId(), entity.getValue());
 12:     // protection
 13:     entity.setId(0);
 14:     // 設置 Item 的行號,以 Namespace 下的 Item 最大行號 + 1 。
 15:     if (entity.getLineNum() == 0) {
 16:         Item lastItem = findLastOne(entity.getNamespaceId());
 17:         int lineNum = lastItem == null ? 1 : lastItem.getLineNum() + 1;
 18:         entity.setLineNum(lineNum);
 19:     }
 20:     // 保存 Item
 21:     Item item = itemRepository.save(entity);
 22:     // 記錄 Audit 到數據庫中
 23:     auditService.audit(Item.class.getSimpleName(), item.getId(), Audit.OP.INSERT, item.getDataChangeCreatedBy());
 24:     return item;
 25: }
  • 第 9 行:調用 #checkItemKeyLength(key) 方法,校驗 Key 長度。

    • 可配置 "item.value.length.limit" 在 ServerConfig 配置最大長度。

    • 默認最大長度爲 128 。

  • 第 11 行:調用 #checkItemValueLength(namespaceId, value) 方法,校驗 Value 長度。

    • 全局可配置 "item.value.length.limit" 在 ServerConfig 配置最大長度。

    • 自定義配置 "namespace.value.length.limit.override" 在 ServerConfig 配置最大長度。

    • 默認最大長度爲 20000 。

  • 第 14 至 19 行:設置 Item 的行號,以 Namespace 下的 Item 最大行號 + 1 。#findLastOne(namespaceId) 方法,獲得最大行號的 Item 對象,代碼如下:

    public Item findLastOne(long namespaceId) {
        return itemRepository.findFirst1ByNamespaceIdOrderByLineNumDesc(namespaceId);
    }
    
  • 第 21 行:調用 ItemRepository#save(Item) 方法,保存 Item 。

  • 第 23 行:記錄 Audit 到數據庫中

4.3 ItemRepository

com.ctrip.framework.apollo.biz.repository.ItemRepository ,繼承 org.springframework.data.repository.PagingAndSortingRepository 接口,提供 Item 的數據訪問 給 Admin Service 和 Config Service 。代碼如下:

public interface ItemRepository extends PagingAndSortingRepository<Item, Long> {

  Item findByNamespaceIdAndKey(Long namespaceId, String key);

  List<Item> findByNamespaceIdOrderByLineNumAsc(Long namespaceId);

  List<Item> findByNamespaceId(Long namespaceId);

  List<Item> findByNamespaceIdAndDataChangeLastModifiedTimeGreaterThan(Long namespaceId, Date date);

  Item findFirst1ByNamespaceIdOrderByLineNumDesc(Long namespaceId);

  @Modifying
  @Query("update Item set isdeleted=1,DataChange_LastModifiedBy = ?2 where namespaceId = ?1")
  int deleteByNamespaceId(long namespaceId, String operator);

}

4.4 CommitService

apollo-biz 項目中,com.ctrip.framework.apollo.biz.service.CommitService ,提供 Commit 的 Service 邏輯給 Admin Service 和 Config Service 。

#save(Commit) 方法,保存 Item 對象 。代碼如下:

@Autowired
private CommitRepository commitRepository;

@Transactional
public Commit save(Commit commit) {
    //protection
    commit.setId(0);
    // 保存 Commit
    return commitRepository.save(commit);
}

4.5 CommitRepository

com.ctrip.framework.apollo.biz.repository.CommitRepository ,繼承 org.springframework.data.repository.PagingAndSortingRepository 接口,提供 Commit 的數據訪問 給 Admin Service 和 Config Service 。代碼如下:

public interface CommitRepository extends PagingAndSortingRepository<Commit, Long> {

  List<Commit> findByAppIdAndClusterNameAndNamespaceNameOrderByIdDesc(String appId, String clusterName,
                                                                      String namespaceName, Pageable pageable);

  @Modifying
  @Query("update Commit set isdeleted=1,DataChange_LastModifiedBy = ?4 where appId=?1 and clusterName=?2 and namespaceName = ?3")
  int batchDelete(String appId, String clusterName, String namespaceName, String operator);

}

666. 彩蛋

Commit 的設計,在我們日常的管理後臺,對重要數據的變更,可以作爲參考。



歡迎加入我的知識星球,一起探討架構,交流源碼。加入方式,長按下方二維碼噢

已在知識星球更新源碼解析如下:

最近更新《芋道 SpringBoot 2.X 入門》系列,已經 20 餘篇,覆蓋了 MyBatis、Redis、MongoDB、ES、分庫分表、讀寫分離、SpringMVC、Webflux、權限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能測試等等內容。

提供近 3W 行代碼的 SpringBoot 示例,以及超 4W 行代碼的電商微服務項目。

獲取方式:點“在看”,關注公衆號並回復 666 領取,更多內容陸續奉上。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章