點擊上方“芋道源碼”,選擇“設爲星標”
做積極的人,而不是積極廢人!
源碼精品專欄
摘要: 原創出處 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 調用。代碼如下:
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 領取,更多內容陸續奉上。