主頁 > 後端開發 > 【知其然,知其所以然】配置中心 Apollo原始碼剖析

【知其然,知其所以然】配置中心 Apollo原始碼剖析

2022-06-22 21:10:37 後端開發

第2章 Apollo原始碼剖析

能力目標

  • 能夠基于Git匯入Apollo原始碼
  • 能夠基于IDEA實作DEBUG分析APP創建
  • 掌握Namespace創建程序
  • 掌握Item創建程序
  • 掌味訓度發布創建程序

1:namespace創建、灰度發布配置、Item創建作為自學

2:客戶端剖析

? 通信->Http、輪詢機制

? 組態檔優先級、快取、關聯關系

? 重繪機制【注解決議】

1 Apollo原始碼搭建

在上一章我們已經學習了Apollo專案實戰,為了更進一步學習Apollo、掌握Apollo作業原理,我們開始學習Apollo原始碼,所以我們先搭建Apollo原始碼環境,

1.1 原始碼下載

我們從github上 https://github.com/ctripcorp/apollo 下載原始碼,下載后的原始碼如下:

file

版本切換至v1.7.1(課程中使用的是1.7.0),如下操作:

file

1.2 匯入資料庫

在專案根路徑下有scripts/sql目錄,下面有2個sql腳本,我們將該腳本匯入到資料庫中,

file

如下圖,在本地mysql上執行這兩個腳本:

file

1.3 apollo-assembly啟動服務

我們啟動Apollo服務,需要同時啟動configservice、adminservice,如果手動啟動比較慢,Apollo幫我們封裝了一個工程apollo-assembly,可以基于該工程同時啟動 apollo-adminserviceapollo-configservice 專案,

修改apollo-configservice的核心組態檔bootstrap.yml添加Eureka不注冊Eureka資料也不獲取Eureka資料,配置如下:

file

完整代碼如下:

eureka:
  instance:
    hostname: ${hostname:localhost}
    preferIpAddress: true
    status-page-url-path: /info
    health-check-url-path: /health
  server:
    peerEurekaNodesUpdateIntervalMs: 60000
    enableSelfPreservation: false
  client:
    serviceUrl:
      # This setting will be overridden by eureka.service.url setting from ApolloConfigDB.ServerConfig or System Property
      # see com.ctrip.framework.apollo.biz.eureka.ApolloEurekaClientConfig
      defaultZone: http://${eureka.instance.hostname}:8080/eureka/
    healthcheck:
      enabled: true
    eurekaServiceUrlPollIntervalSeconds: 60
    fetch-registry: false
    register-with-eureka: false

我們先配置該工程,如下圖:

file

這里的VM optins:

-Dapollo_profile=github
-Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloConfigDB?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Dlogging.file=D:/project/xc-apollo/apollo-assembly.log

引數Program arguments中的兩個引數分別表示啟動configserviceadminservice服務,

啟動完成后,我們請求Eureka http://localhost:8080/

file

PortalService啟動

apollo-portal工程需要單獨啟動,啟動的時候我們也需要配置密碼和日志輸出檔案,如下圖:

file

VM options配置如下:

-Dapollo_profile=github,auth
-Ddev_meta=http://localhost:8080/
-Dserver.port=8070
-Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloPortalDB?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Dlogging.file=D:/project/xc-apollo/apollo-portal.log

啟動完成后,我們接下來訪問控制臺 http://localhost:8070 效果如下:

file

1.4 服務測驗

我們可以先創建一個專案并且app.id=100004458,如下圖:

file

file
在該專案的application.properties中添加一個username引數,如下圖:

file

Apollo提供了內置的測驗服務,該服務會訪問Apollo服務app.id=100004458的專案,我們可以在該工程啟動時配置VM options引數指定Apollo注冊中心地址,如下圖:

file

VM options引數配置如下:

-Denv=dev
-Ddev_meta=http://localhost:8080

啟動程式,我們輸入username回車,可以看到對應資料,如下輸出結果:

Apollo Config Demo. Please input key to get the value. Input quit to exit.
> username
> [apollo-demo][main] INFO  [com.ctrip.framework.apollo.demo.api.SimpleApolloConfigDemo] Loading key : username with value: 張三

2 Portal創建APP

Apollo創建App的程序如果基于控制臺操作是很簡單的,但是Apollo是如何實作的呢,我們接下來進行相關原始碼剖析,

file

創建APP的流程如上圖:

1:用戶在后臺執行創建app,會將請求發送到Portal Service
2:Portal Service將資料保存到Portal DB中
3:Portal Service同時將資料同步到Admin Service中,這個程序是異步的
4:Admin Service將資料保存到Config DB中

2.1 創建APP

創建APP由Portal Service執行,我們從它的JavaBean、Controller、Service、Dao一步一步分析,

2.1.1 物體Bean

1)Table

APP對應的表結構如下:

CREATE TABLE `App` (
  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID',
  `Name` varchar(500) NOT NULL DEFAULT 'default' COMMENT '應用名',
  `OrgId` varchar(32) NOT NULL DEFAULT 'default' COMMENT '部門Id',
  `OrgName` varchar(64) NOT NULL DEFAULT 'default' COMMENT '部門名字',
  `OwnerName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerName',
  `OwnerEmail` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'ownerEmail',
  `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal',
  `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '創建人郵箱前綴',
  `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人郵箱前綴',
  `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改時間',
  PRIMARY KEY (`Id`),
  KEY `AppId` (`AppId`(191)),
  KEY `DataChange_LastTime` (`DataChange_LastTime`),
  KEY `IX_Name` (`Name`(191))
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='應用表';

2)App(Bean)

apollo-common 專案中, com.ctrip.framework.apollo.common.entity.App ,繼承 BaseEntity 抽象類,應用資訊物體,代碼如下:

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

  /**
   * App名字
   */
  @NotBlank(message = "Name cannot be blank")
  @Column(name = "Name", nullable = false)
  private String name;

  /**
   * App.id
   */
  @NotBlank(message = "AppId cannot be blank")
  @Pattern(
      regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR,
      message = InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE
  )
  @Column(name = "AppId", nullable = false)
  private String appId;

  /**
   * 部門編號
   */
  @Column(name = "OrgId", nullable = false)
  private String orgId;

  /**
   * 部門名
   */
  @Column(name = "OrgName", nullable = false)
  private String orgName;

  /***
   * 擁有人名
   * 例如在 Portal 系統中,使用系統的管理員賬號,即 UserPO.username 欄位
   */
  @NotBlank(message = "OwnerName cannot be blank")
  @Column(name = "OwnerName", nullable = false)
  private String ownerName;

  /***
   * 擁有人郵箱
   */
  @NotBlank(message = "OwnerEmail cannot be blank")
  @Column(name = "OwnerEmail", nullable = false)
  private String ownerEmail;
  //...get set 略
  
}
  • ORM 選用 Hibernate 框架,
  • @SQLDelete(...) + @Where(...) 注解,配合 BaseEntity.extends 欄位,實作 App 的邏輯洗掉
  • 欄位比較簡單,

3)BaseEntity(Bean)

com.ctrip.framework.apollo.common.entity.BaseEntity ,是基礎物體抽象類,代碼如下:

@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class BaseEntity {

  /**
   * 編號
   */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "Id")
  private long id;

  /**
   * 是否洗掉
   */
  @Column(name = "IsDeleted", columnDefinition = "Bit default '0'")
  protected boolean isDeleted = false;

  /***
   * 資料創建人
   * 例如在 Portal 系統中,使用系統的管理員賬號,即 UserPO.username 欄位
   */
  @Column(name = "DataChange_CreatedBy", nullable = false)
  private String dataChangeCreatedBy;

  /**
   * 資料創建時間
   */
  @Column(name = "DataChange_CreatedTime", nullable = false)
  private Date dataChangeCreatedTime;

  /**
   * 資料最后更新人
   * 例如在 Portal 系統中,使用系統的管理員賬號,即 UserPO.username 欄位
   */
  @Column(name = "DataChange_LastModifiedBy")
  private String dataChangeLastModifiedBy;
  /**
   * 資料最后更新時間
   */
  @Column(name = "DataChange_LastTime")
  private Date dataChangeLastModifiedTime;

  /**
   * 保存前置方法
   */
  @PrePersist
  protected void prePersist() {
    if (this.dataChangeCreatedTime == null) {
        dataChangeCreatedTime = new Date();
    }
    if (this.dataChangeLastModifiedTime == null) {
        dataChangeLastModifiedTime = new Date();
    }
  }

  /**
   * 更新前置方法
   */
  @PreUpdate
  protected void preUpdate() {
    this.dataChangeLastModifiedTime = new Date();
  }

  /**
   * 洗掉前置方法
   */
  @PreRemove
  protected void preRemove() {
    this.dataChangeLastModifiedTime = new Date();
  }
  
  //get set toString...略
}

部分注解和方法我們說明一下:

  • id 欄位,編號,Long 型,全域自增,
  • isDeleted 欄位,是否洗掉,用于邏輯洗掉的功能,
  • dataChangeCreatedBydataChangeCreatedTime 欄位,實作資料的創建人和時間的記錄,方便追蹤,
  • dataChangeLastModifiedBydataChangeLastModifiedTime 欄位,實作資料的更新人和時間的記錄,方便追蹤,
  • @PrePersist@PreUpdate@PreRemove 注解,CRD 操作前,設定對應的時間欄位
  • 在 Apollo 中,所有物體都會繼承 BaseEntity ,實作公用欄位的統一定義,這種設計值得借鑒,特別是創建時間和更新時間這兩個欄位,特別適合線上追蹤問題和資料同步,

資料為什么要同步呢?

在文初的流程圖中,我們看到 App 創建時,在 Portal Service 存盤完成后,會異步同步到 Admin Service 中,這是為什么呢?

在 Apollo 的架構中,一個環境( Env ) 對應一套 Admin Service 和 Config Service ,
而 Portal Service 會管理所有環境( Env ) ,因此,每次創建 App 后,需要進行同步,

或者說,App 在 Portal Service 中,表示需要管理的 App ,而在 Admin Service 和 Config Service 中,表示存在的 App ,

2.1.2 業務執行流程

1)Controller

apollo-portal 專案中,com.ctrip.framework.apollo.portal.controller.AppController ,提供 App 的 API

創建專案的界面中,點擊【提交】按鈕,呼叫創建 App 的 API

file

處理請求的方法如下:

/***
 * 創建App
 * @param appModel AppModel 物件
 * @return
 */
@PreAuthorize(value = "https://www.cnblogs.com/jiagooushi/archive/2022/06/22/@permissionValidator.hasCreateApplicationPermission()")
@PostMapping
public App create(@Valid @RequestBody AppModel appModel) {
  // 將 AppModel 轉換成 App 物件
  App app = transformToApp(appModel);
  // 保存 App 物件到資料庫
  App createdApp = appService.createAppInLocal(app);
  // 發布 AppCreationEvent 創建事件
  publisher.publishEvent(new AppCreationEvent(createdApp));
  // 授予 App 管理員的角色
  Set<String> admins = appModel.getAdmins();
  if (!CollectionUtils.isEmpty(admins)) {
    rolePermissionService
        .assignRoleToUsers(RoleUtils.buildAppMasterRoleName(createdApp.getAppId()),
            admins, userInfoHolder.getUser().getUserId());
  }
  // 回傳 App 物件
  return createdApp;
}

關于創建app請求操作我們做一下說明:

1:POST apps 介面,Request Body 傳遞 JSON 物件,

2:com.ctrip.framework.apollo.portal.entity.model.AppModel ,App Model ,在 com.ctrip.framework.apollo.portal.entity.model 包下,負責接收來自 Portal 界面的復雜請求物件,例如,AppModel 一方面帶有創建 App 物件需要的屬性,另外也帶有需要授權管理員的編號集合 admins ,即存在跨模塊的情況,

3:呼叫 #transformToApp(AppModel) 方法,將 AppModel 轉換成 App 物件,轉換方法很簡單,點擊方法,直接查看,

4:呼叫 AppService#createAppInLocal(App) 方法,保存 App 物件到 Portal DB 資料庫,在 「3.2 AppService」 中,詳細決議,

5:呼叫 ApplicationEventPublisher#publishEvent(AppCreationEvent) 方法,發布 com.ctrip.framework.apollo.portal.listener.AppCreationEvent 事件,

6:授予 App 管理員的角色,詳細決議,見 《Apollo 原始碼決議 —— Portal 認證與授權(二)之授權》 ,

7:回傳創建的 App 物件,

2)Service

apollo-portal 專案中,com.ctrip.framework.apollo.portal.service.AppService ,提供 App 的 Service邏輯,

#createAppInLocal(App) 方法,保存 App 物件到 Portal DB 數庫,代碼如下:

@Transactional
public App createAppInLocal(App app) {
  String appId = app.getAppId();
  // 判斷 `appId` 是否已經存在對應的 App 物件,若已經存在,拋出 BadRequestException 例外,
  App managedApp = appRepository.findByAppId(appId);
  if (managedApp != null) {
    throw new BadRequestException(String.format("App already exists. AppId = %s", appId));
  }
  // 獲得 UserInfo 物件,若不存在,拋出 BadRequestException 例外
  UserInfo owner = userService.findByUserId(app.getOwnerName());
  if (owner == null) {
    throw new BadRequestException("Application's owner not exist.");
  }
  // Email
  app.setOwnerEmail(owner.getEmail());
  // 設定 App 的創建和修改人
  String operator = userInfoHolder.getUser().getUserId();
  app.setDataChangeCreatedBy(operator);
  app.setDataChangeLastModifiedBy(operator);
  // 保存 App 物件到資料庫
  App createdApp = appRepository.save(app);
  // 創建 App 的默認命名空間 "application"
  appNamespaceService.createDefaultAppNamespace(appId);
  // 初始化 App 角色
  roleInitializationService.initAppRoles(createdApp);
  // Tracer 日志
  Tracer.logEvent(TracerEventType.CREATE_APP, appId);
  return createdApp;
}

所有代碼執行程序,我們已經在代碼中標注了,大家可以按執行流程查看,

3)AppRepository

在 apollo-portal 專案中,com.ctrip.framework.apollo.common.entity.App.AppRepository ,繼承 org.springframework.data.repository.PagingAndSortingRepository 介面,提供 App 的資料訪問,即 DAO ,

代碼如下:

public interface AppRepository extends PagingAndSortingRepository<App, Long> {

  App findByAppId(String appId);

  List<App> findByOwnerName(String ownerName, Pageable page);

  List<App> findByAppIdIn(Set<String> appIds);

  List<App> findByAppIdIn(Set<String> appIds, Pageable pageable);

  Page<App> findByAppIdContainingOrNameContaining(String appId, String name, Pageable pageable);

  @Modifying
  @Query("UPDATE App SET IsDeleted=1,DataChange_LastModifiedBy = ?2 WHERE AppId=?1")
  int deleteApp(String appId, String operator);
}

持久層是基于 Spring Data JPA 框架,使用 Hibernate 實作,

2.2 資料同步

在前面流程圖中我們說過會呼叫Admin Service執行同步,同步程序是如何同步的呢,其實這里采用了觀察者模式進行了監聽操作,我們一起來分析一下,

2.2.1 觀察者模式

定義:

物件之間存在一對多或者一對一依賴,當一個物件改變狀態,依賴它的物件會收到通知并自動更新,
MQ其實就屬于一種觀察者模式,發布者發布資訊,訂閱者獲取資訊,訂閱了就能收到資訊,沒訂閱就收不到資訊,

優點:

1:觀察者和被觀察者是抽象耦合的, 
2:建立一套觸發機制,

缺點:

1:如果一個被觀察者物件有很多的直接和間接的觀察者的話,將所有的觀察者都通知到會花費很多時間, 
2:如果在觀察者和觀察目標之間有回圈依賴的話,觀察目標會觸發它們之間進行回圈呼叫,可能導致系統崩潰, 

Spring觀察者模式

ApplicationContext事件機制是觀察者設計模式的實作,通過ApplicationEvent類和ApplicationListener介面,可以實作ApplicationContext事件處理,

如果容器中有一個ApplicationListener Bean,每當ApplicationContext發布ApplicationEvent時,ApplicationListener Bean將自動被觸發,這種事件機制都必須需要程式顯示的觸發,

其中spring有一些內置的事件,當完成某種操作時會發出某些事件動作,比如監聽ContextRefreshedEvent事件,當所有的bean都初始化完成并被成功裝載后會觸發該事件,實作ApplicationListener<ContextRefreshedEvent>介面可以收到監聽動作,然后可以寫自己的邏輯,

同樣事件可以自定義、監聽也可以自定義,完全根據自己的業務邏輯來處理,

2.2.2 事件監聽

在Portal Service創建APP的controller中會創建時間監聽,代碼如下:

file

事件監聽創建后,Portal Service中有一個監聽創建監聽物件,在該監聽物件中會監聽創建事件資訊,并根據創建的APP進行同步呼叫,主要呼叫的是AppAPI,而AppAPI是執行遠程操作,代碼如下:

@Component
public class CreationListener {

  private final AdminServiceAPI.AppAPI appAPI;

  /***
   * 監聽
   * @param event
   */
  @EventListener
  public void onAppCreationEvent(AppCreationEvent event) {
    // 將 App 轉成 AppDTO 物件
    AppDTO appDTO = BeanUtils.transform(AppDTO.class, event.getApp());
    // 獲得有效的 Env 陣列
    List<Env> envs = portalSettings.getActiveEnvs();
    // 回圈 Env 陣列,呼叫對應的 Admin Service 的 API ,創建 App 物件,
    for (Env env : envs) {
      try {
        appAPI.createApp(env, appDTO);
      } catch (Throwable e) {
        logger.error("Create app failed. appId = {}, env = {})", appDTO.getAppId(), env, e);
        Tracer.logError(String.format("Create app failed. appId = %s, env = %s", appDTO.getAppId(), env), e);
      }
    }
  }
}

AppAPI使用了RestTemplate執行遠程操作,代碼如下:

file

2.2.3 同步業務執行流程

apollo-adminservice 專案中, com.ctrip.framework.apollo.adminservice.controller.AppController ,提供 App 的 API

#create(AppDTO) 方法,創建 App ,代碼如下:

/***
 * 創建App
 * @param dto
 * @return
 */
@PostMapping("/apps")
public AppDTO create(@Valid @RequestBody AppDTO dto) {
  // 將 AppDTO 轉換成 App 物件
  App entity = BeanUtils.transform(App.class, dto);
  App managedEntity = appService.findOne(entity.getAppId());
  // 判斷 `appId` 是否已經存在對應的 App 物件,若已經存在,拋出 BadRequestException 例外,
  if (managedEntity != null) {
    throw new BadRequestException("app already exist.");
  }
  // 保存 App 物件到資料庫
  entity = adminService.createNewApp(entity);
  // 將保存的 App 物件,轉換成 AppDTO 回傳
  return BeanUtils.transform(AppDTO.class, entity);
}

com.ctrip.framework.apollo.biz.service.AdminService#createNewApp(App) 方法,代碼如下:

@Transactional
public App createNewApp(App app) {
  // 保存 App 物件到資料庫
  String createBy = app.getDataChangeCreatedBy();
  App createdApp = appService.save(app);
  String appId = createdApp.getAppId();
  // 創建 App 的默認命名空間 "application"
  appNamespaceService.createDefaultAppNamespace(appId, createBy);
  // 創建 App 的默認集群 "default"
  clusterService.createDefaultCluster(appId, createBy);
  // 創建 Cluster 的默認命名空間
  namespaceService.instanceOfAppNamespaces(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, createBy);
  return app;
}

apollo-biz 專案中,com.ctrip.framework.apollo.biz.service.AppService ,提供 App 的 Service 邏輯給 Admin Service 和 Config Service ,

#save(App) 方法,保存 App 物件到資料庫中,代碼如下:

@Transactional
public App save(App entity) {
  // 判斷是否已經存在,若是,拋出 ServiceException 例外,
  if (!isAppIdUnique(entity.getAppId())) {
    throw new ServiceException("appId not unique");
  }
  // 保護代碼,避免 App 物件中,已經有 id 屬性,
  entity.setId(0);//protection
  App app = appRepository.save(entity);
  // 記錄 Audit 到資料庫中
  auditService.audit(App.class.getSimpleName(), app.getId(), Audit.OP.INSERT,
      app.getDataChangeCreatedBy());
  return app;
}

至于Dao還是JPA操作,我們不再過多講解了,

3 Namespace創建

file

namespace創建的流程也是先經過Portal Service,再同步到Admin Service中,執行流程我們先來一起分析一下:

file

這里我們發現有AppNamespace和Namespace,他們有一定區別:

資料流向如下:
   	在App下創建 AppNamespace 后,自動給 App 下每個 Cluster 創建 Namespace ,
	在App下創建 Cluster 后,根據 App 下 每個 AppNamespace 創建 Namespace ,
	可洗掉 Cluster 下的 Namespace ,
總結來說:
	AppNamespace 是 App 下的每個 Cluster 默認創建的 Namespace ,
	Namespace 是 每個 Cluster 實際擁有的 Namespace ,

Namespace 型別有三種:

1:私有型別:私有型別的 Namespace 具有 private 權限,
2:公共型別:公共型別的 Namespace 具有 public 權限,公共型別的 Namespace 相當于游離于應用之外的配置,且通過 Namespace 的名稱去標識公共 Namespace ,所以公共的 Namespace 的名稱必須全域唯一,
3:關聯型別:關聯型別又可稱為繼承型別,關聯型別具有 private 權限,關聯型別的Namespace 繼承于公共型別的Namespace,用于覆寫公共 Namespace 的某些配置,

我們接下來對該執行流程的原始碼進行剖析,

3.1 創建AppNamespace

AppNamespace創建由Portal Service發起,我們先來分析該工程,

3.1.1 物體Bean

1)Table

AppNamespace對應表表結構如下:

CREATE TABLE `AppNamespace` (
  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `Name` varchar(32) NOT NULL DEFAULT '' COMMENT 'namespace名字,注意,需要全域唯一',
  `AppId` varchar(32) NOT NULL DEFAULT '' COMMENT 'app id',
  `Format` varchar(32) NOT NULL DEFAULT 'properties' COMMENT 'namespace的format型別',
  `IsPublic` bit(1) NOT NULL DEFAULT b'0' COMMENT 'namespace是否為公共',
  `Comment` varchar(64) NOT NULL DEFAULT '' COMMENT '注釋',
  `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal',
  `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT '' COMMENT '創建人郵箱前綴',
  `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人郵箱前綴',
  `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改時間',
  PRIMARY KEY (`Id`),
  KEY `IX_AppId` (`AppId`),
  KEY `Name_AppId` (`Name`,`AppId`),
  KEY `DataChange_LastTime` (`DataChange_LastTime`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='應用namespace定義';

Namespace表結構如下:

CREATE TABLE `Namespace` (
  `Id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `AppId` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'AppID',
  `ClusterName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Cluster Name',
  `NamespaceName` varchar(500) NOT NULL DEFAULT 'default' COMMENT 'Namespace Name',
  `IsDeleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '1: deleted, 0: normal',
  `DataChange_CreatedBy` varchar(32) NOT NULL DEFAULT 'default' COMMENT '創建人郵箱前綴',
  `DataChange_CreatedTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `DataChange_LastModifiedBy` varchar(32) DEFAULT '' COMMENT '最后修改人郵箱前綴',
  `DataChange_LastTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改時間',
  PRIMARY KEY (`Id`),
  KEY `AppId_ClusterName_NamespaceName` (`AppId`(191),`ClusterName`(191),`NamespaceName`(191)),
  KEY `DataChange_LastTime` (`DataChange_LastTime`),
  KEY `IX_NamespaceName` (`NamespaceName`(191))
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='命名空間';

2)物體Bean

apollo-common 專案中,com.ctrip.framework.apollo.common.entity.AppNamespace ,繼承 BaseEntity 抽象類,App Namespace 物體,代碼如下:

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

  /**
   * AppNamespace 名
   */
  @NotBlank(message = "AppNamespace Name cannot be blank")
  @Pattern(
      regexp = InputValidator.CLUSTER_NAMESPACE_VALIDATOR,
      message = "Invalid Namespace format: " + InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & " + InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE
  )
  @Column(name = "Name", nullable = false)
  private String name;

  /**
   * App 編號
   */
  @NotBlank(message = "AppId cannot be blank")
  @Column(name = "AppId", nullable = false)
  private String appId;

  /**
   * 格式
   * 參見 {@link ConfigFileFormat}
   */
  @Column(name = "Format", nullable = false)
  private String format;

  /**
   * 是否公用的
   */
  @Column(name = "IsPublic", columnDefinition = "Bit default '0'")
  private boolean isPublic = false;

  /**
   * 備注
   */
  @Column(name = "Comment")
  private String comment;
  
  //get set toString...略
  
}
  • appId 欄位,App 編號,指向對應的 App ,App : AppNamespace = 1 : N ,
  • format 欄位,格式,在 com.ctrip.framework.apollo.core.enums.ConfigFileFormat 列舉類中,定義了6種型別:Properties("properties"), XML("xml"), JSON("json"), YML("yml"), YAML("yaml"), TXT("txt");
  • 欄位,是否公用的
  • Namespace的獲取權限分為兩種:
    • private (私有的):private 權限的 Namespace ,只能被所屬的應用獲取到,一個應用嘗試獲取其它應用 private 的 Namespace ,Apollo 會報 “404” 例外,
    • public (公共的):public 權限的 Namespace ,能被任何應用獲取,

apollo-biz 專案中, com.ctrip.framework.apollo.biz.entity.Namespace ,繼承 BaseEntity 抽象類,Cluster Namespace 物體,是配置項的集合,類似于一個組態檔的概念,代碼如下:

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

  /**
   * App 編號 {@link com.ctrip.framework.apollo.common.entity.App#appId}
   */
  @Column(name = "appId", nullable = false)
  private String appId;

  /**
   * Cluster 名 {@link Cluster#name}
   */
  @Column(name = "ClusterName", nullable = false)
  private String clusterName;
  /**
   * AppNamespace 名 {@link com.ctrip.framework.apollo.common.entity.AppNamespace#name}
   */
  @Column(name = "NamespaceName", nullable = false)
  private String namespaceName;
  
  //get ..set ..toString..略
}

3.1.2 業務執行流程

1)Controller

提交業務請求會呼叫apollo-portalcom.ctrip.framework.apollo.portal.controller.NamespaceController,Portal Service提供了提供 AppNamespace 和 Namespace 的 API

com.ctrip.framework.apollo.portal.controller.NamespaceController創建AppNamespace方法原始碼如下:

@PreAuthorize(value = "https://www.cnblogs.com/jiagooushi/archive/2022/06/22/@permissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)")
@PostMapping("/apps/{appId}/appnamespaces")
public AppNamespace createAppNamespace(@PathVariable String appId,
    @RequestParam(defaultValue = "https://www.cnblogs.com/jiagooushi/archive/2022/06/22/true") boolean appendNamespacePrefix,
    @Valid @RequestBody AppNamespace appNamespace) {
  // 校驗 AppNamespace 的 `name` 非空,
  if (!InputValidator.isValidAppNamespace(appNamespace.getName())) {
    throw new BadRequestException(String.format("Invalid Namespace format: %s",
        InputValidator.INVALID_CLUSTER_NAMESPACE_MESSAGE + " & " + InputValidator.INVALID_NAMESPACE_NAMESPACE_MESSAGE));
  }
  // 保存 AppNamespace 物件到資料庫
  AppNamespace createdAppNamespace = appNamespaceService.createAppNamespaceInLocal(appNamespace, appendNamespacePrefix);
  // 賦予權限,若滿足如下任一條件:
  // 1. 公開型別的 AppNamespace ,
  // 2. 私有型別的 AppNamespace ,并且允許 App 管理員創建私有型別的 AppNamespace ,
  if (portalConfig.canAppAdminCreatePrivateNamespace() || createdAppNamespace.isPublic()) {
    // 授予 Namespace Role
    namespaceService.assignNamespaceRoleToOperator(appId, appNamespace.getName(),
        userInfoHolder.getUser().getUserId());
  }
  // 發布 AppNamespaceCreationEvent 創建事件
  publisher.publishEvent(new AppNamespaceCreationEvent(createdAppNamespace));
  // 回傳創建的 AppNamespace 物件
  return createdAppNamespace;
}

在這里我們不難發現它又創建了監聽,所以肯定也會涉及資料同步,

2)Service

apollo-portal 專案中,com.ctrip.framework.apollo.portal.service.AppNamespaceService ,提供 AppNamespace 的 Service 邏輯,

#createAppNamespaceInLocal(AppNamespace) 方法,保存 AppNamespace 物件到 Portal DB 資料庫,代碼如下:

@Transactional
public AppNamespace createAppNamespaceInLocal(AppNamespace appNamespace, boolean appendNamespacePrefix) {
  String appId = appNamespace.getAppId();
  // 校驗對應的 App 是否存在,若不存在,拋出 BadRequestException 例外
  //add app org id as prefix
  App app = appService.load(appId);
  if (app == null) {
    throw new BadRequestException("App not exist. AppId = " + appId);
  }

  // public namespaces only allow properties format
  if (appNamespace.isPublic()) {
    appNamespace.setFormat(ConfigFileFormat.Properties.getValue());
  }
  // 拼接 AppNamespace 的 `name` 屬性,
  StringBuilder appNamespaceName = new StringBuilder();
  //add prefix postfix
  appNamespaceName
      .append(appNamespace.isPublic() && appendNamespacePrefix ? app.getOrgId() + "." : "")
      .append(appNamespace.getName())
      .append(appNamespace.formatAsEnum() == ConfigFileFormat.Properties ? "" : "." + appNamespace.getFormat());
  appNamespace.setName(appNamespaceName.toString());

  // 設定 AppNamespace 的 `comment` 屬性為空串,若為 null ,
  if (appNamespace.getComment() == null) {
    appNamespace.setComment("");
  }
  // 校驗 AppNamespace 的 `format` 是否合法
  if (!ConfigFileFormat.isValidFormat(appNamespace.getFormat())) {
   throw new BadRequestException("Invalid namespace format. format must be properties、json、yaml、yml、xml");
  }
  // 設定 AppNamespace 的創建和修改人
  String operator = appNamespace.getDataChangeCreatedBy();
  if (StringUtils.isEmpty(operator)) {
    operator = userInfoHolder.getUser().getUserId();
    appNamespace.setDataChangeCreatedBy(operator);
  }
  appNamespace.setDataChangeLastModifiedBy(operator);
  //公用型別,校驗 `name` 在全域唯一
  // globally uniqueness check for public app namespace
  if (appNamespace.isPublic()) {
    checkAppNamespaceGlobalUniqueness(appNamespace);
  } else {
    // 私有型別,校驗 `name` 在 App 下唯一
    // check private app namespace
    if (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) {
      throw new BadRequestException("Private AppNamespace " + appNamespace.getName() + " already exists!");
    }
    // should not have the same with public app namespace
    checkPublicAppNamespaceGlobalUniqueness(appNamespace);
  }
  // 保存 AppNamespace 到資料庫
  AppNamespace createdAppNamespace = appNamespaceRepository.save(appNamespace);

  roleInitializationService.initNamespaceRoles(appNamespace.getAppId(), appNamespace.getName(), operator);
  roleInitializationService.initNamespaceEnvRoles(appNamespace.getAppId(), appNamespace.getName(), operator);

  return createdAppNamespace;
}

關于Dao我們就不做分析了,

3.2 資料同步

3.2.1 事件監聽

com.ctrip.framework.apollo.portal.listener.CreationListener物件創建監聽器,目前監聽 AppCreationEvent 和 AppNamespaceCreationEvent 事件,

我們看看com.ctrip.framework.apollo.portal.listener.CreationListener#onAppNamespaceCreationEvent代碼如下:

@EventListener
public void onAppNamespaceCreationEvent(AppNamespaceCreationEvent event) {
  // 將 AppNamespace 轉成 AppNamespaceDTO 物件
  AppNamespaceDTO appNamespace = BeanUtils.transform(AppNamespaceDTO.class, event.getAppNamespace());
  // 獲得有效的 Env 陣列
  List<Env> envs = portalSettings.getActiveEnvs();
  // 回圈 Env 陣列,呼叫對應的 Admin Service 的 API ,創建 AppNamespace 物件,
  for (Env env : envs) {
    try {
      namespaceAPI.createAppNamespace(env, appNamespace);
    } catch (Throwable e) {
      logger.error("Create appNamespace failed. appId = {}, env = {}", appNamespace.getAppId(), env, e);
      Tracer.logError(String.format("Create appNamespace failed. appId = %s, env = %s", appNamespace.getAppId(), env), e);
    }
  }
}

上面監聽仍然會呼叫遠程服務,使用了namespaceAPI執行了遠程呼叫,部分原始碼如下:

file

3.2.2 同步業務執行流程

1)Controller

apollo-adminservice 專案中, com.ctrip.framework.apollo.adminservice.controller.AppNamespaceController ,提供 AppNamespace 的 API

#create(AppNamespaceDTO) 方法,創建 AppNamespace ,代碼如下:

/**
 * 創建 AppNamespace
 * @param appNamespace
 * @param silentCreation
 * @return
 */
@PostMapping("/apps/{appId}/appnamespaces")
public AppNamespaceDTO create(@RequestBody AppNamespaceDTO appNamespace,
                              @RequestParam(defaultValue = "https://www.cnblogs.com/jiagooushi/archive/2022/06/22/false") boolean silentCreation) {
  // 將 AppNamespaceDTO 轉換成 AppNamespace 物件
  AppNamespace entity = BeanUtils.transform(AppNamespace.class, appNamespace);
  // 判斷 `name` 在 App 下是否已經存在對應的 AppNamespace 物件,若已經存在,拋出 BadRequestException 例外,
  AppNamespace managedEntity = appNamespaceService.findOne(entity.getAppId(), entity.getName());
  if (managedEntity == null) {
    if (StringUtils.isEmpty(entity.getFormat())){
      entity.setFormat(ConfigFileFormat.Properties.getValue());
    }
    //不存在,就添加AppNamespace
    entity = appNamespaceService.createAppNamespace(entity);
  } else if (silentCreation) {
    appNamespaceService.createNamespaceForAppNamespaceInAllCluster(appNamespace.getAppId(), appNamespace.getName(),
        appNamespace.getDataChangeCreatedBy());

    entity = managedEntity;
  } else {
    throw new BadRequestException("app namespaces already exist.");
  }

  return BeanUtils.transform(AppNamespaceDTO.class, entity);
}

2)Service

apollo-biz 專案中,com.ctrip.framework.apollo.biz.service.AppNamespaceService ,提供 AppNamespace 的 Service 邏輯給 Admin Service 和 Config Service ,

#save(AppNamespace) 方法,保存 AppNamespace 物件到資料庫中,代碼如下:

@Transactional
public AppNamespace createAppNamespace(AppNamespace appNamespace) {
  // 判斷 `name` 在 App 下是否已經存在對應的 AppNamespace 物件,若已經存在,拋出 ServiceException 例外,
  String createBy = appNamespace.getDataChangeCreatedBy();
  if (!isAppNamespaceNameUnique(appNamespace.getAppId(), appNamespace.getName())) {
    throw new ServiceException("appnamespace not unique");
  }
  // 保護代碼,避免 App 物件中,已經有 id 屬性,
  appNamespace.setId(0);//protection
  appNamespace.setDataChangeCreatedBy(createBy);
  appNamespace.setDataChangeLastModifiedBy(createBy);
  // 保存 AppNamespace 到資料庫
  appNamespace = appNamespaceRepository.save(appNamespace);
  // 創建 AppNamespace 在 App 下,每個 Cluster 的 Namespace 物件,
  createNamespaceForAppNamespaceInAllCluster(appNamespace.getAppId(), appNamespace.getName(), createBy);
  // 記錄 Audit 到資料庫中
  auditService.audit(AppNamespace.class.getSimpleName(), appNamespace.getId(), Audit.OP.INSERT, createBy);
  return appNamespace;
}

呼叫 #instanceOfAppNamespaceInAllCluster(appId, namespaceName, createBy) 方法,創建 AppNamespace 在 App 下,每個 Cluster 的 Namespace 物件,代碼如下:

file

注意這里每次都呼叫了namespaceService.save()方法,該方法會保存Namespace,

apollo-biz 專案中,com.ctrip.framework.apollo.biz.service.NamespaceService ,提供 Namespace 的 Service 邏輯給 Admin Service 和 Config Service ,

#save(Namespace) 方法,保存 Namespace 物件到資料庫中,代碼如下:

@Transactional
public Namespace save(Namespace entity) {
  // 判斷是否已經存在,若是,拋出 ServiceException 例外,
  if (!isNamespaceUnique(entity.getAppId(), entity.getClusterName(), entity.getNamespaceName())) {
    throw new ServiceException("namespace not unique");
  }
  // 保護代碼,避免 Namespace 物件中,已經有 id 屬性,
  entity.setId(0);//protection
  // 保存 Namespace 到資料庫
  Namespace namespace = namespaceRepository.save(entity);
  // 記錄 Audit 到資料庫中
  auditService.audit(Namespace.class.getSimpleName(), namespace.getId(), Audit.OP.INSERT,
                     namespace.getDataChangeCreatedBy());
  return namespace;
}

4 Apollo客戶端

我們接下來分析一下Apollo客戶端是如何獲取Apollo配置資訊的,

4.1 Spring擴展

我們要想實作Apollo和Spring無縫整合,需要在Spring容器重繪之前,從Apollo服務器拉取組態檔,并注入到Spring容器指定變數中,此時可以利用ApplicationContextInitializer物件,

ConfigurableApplicationContext:可以操作組態檔資訊,代碼如下:

public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {

    /**
     * 應用背景關系配置時,這些符號用于分割多個配置路徑
     */
    String CONFIG_LOCATION_DELIMITERS = ",; \t\n";

    /**
     * Environment類在容器中實體的名字
     */
    String ENVIRONMENT_BEAN_NAME = "environment";

    /**
     * System系統變數在容器中對應的Bean的名字
     */
    String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties";

    /**
     * System 環境變數在容器中對應的Bean的名字
     */
    String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";

    /**
     * 設定容器的Environment變數:可以利用當前物件ConfigurableApplicationContext實作對變數的配置
     */
    void setEnvironment(ConfigurableEnvironment environment);

    /**
     * 此方法一般在讀取應用背景關系配置的時候呼叫,用以向此容器中增加BeanFactoryPostProcessor,
     * 增加的Processor會在容器refresh的時候使用,
     */
    void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor);

    /**
     * 向容器增加一個ApplicationListener,增加的Listener用于發布背景關系事件如refresh和shutdown等
     * 需要注意的是,如果此背景關系還沒有啟動,那么在此注冊的Listener將會在背景關系refresh的時候,全部被呼叫
     * 如果背景關系已經是active狀態的了,就會在multicaster中使用
     */
    void addApplicationListener(ApplicationListener<?> listener);

    /**
     * 加載資源組態檔(XML、properties,Whatever),
     * 由于此方法是一個初始化方法,因此如果呼叫此方法失敗的情況下,要將其已經創建的Bean銷毀,
     * 換句話說,呼叫此方法以后,要么所有的Bean都實體化好了,要么就一個都沒有實體化
     */
    void refresh() throws BeansException, IllegalStateException;
}

ApplicationContextInitializer是Spring框架原有的東西,這個類的主要作用就是在ConfigurableApplicationContext型別(或者子型別)的ApplicationContext做refresh之前,允許我們對ConfiurableApplicationContext的實體做進一步的設定和處理,

ApplicationContextInitializer:代碼如下

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    /**
     * 容器重繪之前呼叫該放啊
     */
    void initialize(C applicationContext);
}

4.2 Apollo擴展Spring

Apollo利用Spring擴展機制實作了先從Apollo加載配置,并決議配置,再將資料添加到ConfigurableApplicationContext中,從而實作配置有限加載:

public class ApolloApplicationContextInitializer implements
    ApplicationContextInitializer<ConfigurableApplicationContext> , EnvironmentPostProcessor, Ordered {


  @Override
  public void initialize(ConfigurableApplicationContext context) {
    //從ConfigurableApplicationContext獲取Environment
    ConfigurableEnvironment environment = context.getEnvironment();

    //初始化加載
    initialize(environment);
  }


  /**
   * Initialize Apollo Configurations Just after environment is ready.
   *
   * @param environment
   */
  protected void initialize(ConfigurableEnvironment environment) {

    if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
      //already initialized
      return;
    }

    String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
    logger.debug("Apollo bootstrap namespaces: {}", namespaces);
    //獲取所有namespace,也就是apollo.bootstrap.namespaces的值
    List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);

    CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
    //回圈所有namespace獲取每個namespace的值
    for (String namespace : namespaceList) {
      //ConfigServiceLocator.updateConfigServices執行http請求獲取資料
      Config config = ConfigService.getConfig(namespace);
      composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
    }
    //將資料添加到environment中
    environment.getPropertySources().addFirst(composite);
  }

}

4.3 資料同步

ApolloApplicationContextInitializer.initialize():
	for (String namespace : namespaceList) {
      //呼叫->DefaultConfigManager.getConfig()
      Config config = ConfigService.getConfig(namespace);
      System.out.println(namespace+"-->"+config);
      composite.addPropertySource(x);
    }

DefaultConfigManager.getConfig():
	//為每個命名空間創建(獲取)組態檔,呼叫->DefaultConfigFactory.create()
	config = factory.create(namespace);

DefaultConfigFactory.create():
	//RemoteConfigRepository核心代碼
	createLocalConfigRepository(namespace)  ↓
    return new RemoteConfigRepository(namespace)
        
RemoteConfigRepository.RemoteConfigRepository():
	//1:同步資料
	AbstractConfigRepository.trySync()->AbstractConfigRepository.sync()
	//2:為每個命名空間創建定時任務,定時同步配置,默認5min更新1次
	RemoteConfigRepository.schedulePeriodicRefresh()->AbstractConfigRepository.trySync()
	//3:為每個命名空間創建輪詢任務,輪詢更新集群配置
	RemoteConfigRepository.scheduleLongPollingRefresh()

4.4 @ApolloConfigChangeListener

@ApolloConfigChangeListener注解是監聽注解,當Apollo組態檔發生變更時,用該注解標注的方法會立刻得到通知,我們來看下方法:
file

該注解涉及到時間物件ConfigChangeEvent,該物件資訊如下:

public class ConfigChangeEvent {
  //命名空間
  private final String m_namespace;
  //變更資料
  private final Map<String, ConfigChange> m_changes;
}

上面變更資料用到了一個物件記錄ConfigChange,原始碼如下:

public class ConfigChange {
  //命名空間
  private final String namespace;
  //屬性名字
  private final String propertyName;
  //原始值
  private String oldValue;
  //新值
  private String newValue;
  //操作型別
  private PropertyChangeType changeType;
}

1)監聽器添加

ApolloAnnotationProcessor前置攔截器,為每個namespace添加監聽器:

/***
 * 方法處理
 * @param bean
 * @param beanName
 * @param method
 */
@Override
protected void processMethod(final Object bean, String beanName, final Method method) {
  //檢查該方法是否有@ApolloConfigChangeListener注解
  ApolloConfigChangeListener annotation = AnnotationUtils
      .findAnnotation(method, ApolloConfigChangeListener.class);
  //沒有就直接回傳
  if (annotation == null) {
    return;
  }
  //獲取引數型別集合
  Class<?>[] parameterTypes = method.getParameterTypes();
  Preconditions.checkArgument(parameterTypes.length == 1,
      "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length,
      method);
  Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
      "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0],
      method);
  //暴力破解
  ReflectionUtils.makeAccessible(method);
  //獲取命名空間
  String[] namespaces = annotation.value();
  //獲取要監聽的key
  String[] annotatedInterestedKeys = annotation.interestedKeys();
  //獲取要監聽的key的前綴集合
  String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();
  //創建監聽
  ConfigChangeListener configChangeListener = new ConfigChangeListener() {
    @Override
    public void onChange(ConfigChangeEvent changeEvent) {
      //執行方法呼叫
      ReflectionUtils.invokeMethod(method, bean, changeEvent);
    }
  };

  Set<String> interestedKeys = annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null;
  Set<String> interestedKeyPrefixes = annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) : null;

  // 給config設定listener
  for (String namespace : namespaces) {
    Config config = ConfigService.getConfig(namespace);
    //為每個命名空間添加configChangeListener,當每個命名空間發生變化的時候,都會觸發該configChangeListener執行
    if (interestedKeys == null && interestedKeyPrefixes == null) {
      config.addChangeListener(configChangeListener);
    } else {
      config.addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes);
    }
  }
}

2)監聽器執行

監聽器執行在執行同步發現資料變更的時候執行,其中RemoteConfigRepository.sync()例子如下:

本文由傳智教育博學谷 - 狂野架構師教研團隊發布
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支持是我堅持創作的動力
轉載請注明出處!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/494453.html

標籤:其他

上一篇:React-為react-i18next呼叫useTranslation時掛鉤呼叫無效

下一篇:如何從oraclesql中的表中顯示總和,運行總和和季度總和

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more