关于java:知其然知其所以然配置中心-Apollo源码剖析

57次阅读

共计 31878 个字符,预计需要花费 80 分钟才能阅读完成。

第 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/… 下载源码,下载后的源码如下:

版本切换至 v1.7.1(课程中应用的是 1.7.0), 如下操作:

1.2 导入数据库

在我的项目根门路下有 scripts/sql 目录,上面有 2 个 sql 脚本,咱们将该脚本导入到数据库中。

如下图,在本地 mysql 上执行这两个脚本:

1.3 apollo-assembly 启动服务

咱们启动 Apollo 服务,须要同时启动 configservice、adminservice,如果手动启动比较慢,Apollo 帮咱们封装了一个工程apollo-assembly,能够基于该工程同时启动 apollo-adminserviceapollo-configservice 我的项目。

批改 apollo-configservice 的外围配置文件 bootstrap.yml 增加 Eureka 不注册 Eureka 数据也不获取 Eureka 数据,配置如下:

残缺代码如下:

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

咱们先配置该工程,如下图:

这里的 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/

PortalService 启动

apollo-portal 工程须要独自启动,启动的时候咱们也须要配置明码和日志输入文件,如下图:

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 成果如下:

1.4 服务测试

咱们能够先创立一个我的项目并且 app.id=100004458, 如下图:


在该项目标 application.properties 中增加一个 username 参数,如下图:

Apollo提供了内置的测试服务,该服务会拜访 Apollo 服务 app.id=100004458 的我的项目,咱们能够在该工程启动时配置 VM options 参数指定 Apollo 注册核心地址,如下图:

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 是如何实现的呢,咱们接下来进行相干源码分析。

创立 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

解决申请的办法如下:

/***
 * 创立 App
 * @param appModel AppModel 对象
 * @return
 */
@PreAuthorize(value = "@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 中会创立工夫监听,代码如下:

事件监听创立后,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 执行近程操作,代码如下:

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 创立

namespace 创立的流程也是先通过 Portal Service,再同步到 Admin Service 中,执行流程咱们先来一起剖析一下:

这里咱们发现有 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 = "@permissionValidator.hasCreateAppNamespacePermission(#appId, #appNamespace)")
@PostMapping("/apps/{appId}/appnamespaces")
public AppNamespace createAppNamespace(@PathVariable String appId,
    @RequestParam(defaultValue = "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 执行了近程调用,局部源码如下:

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 = "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 对象。代码如下:

留神这里每次都调用了 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 配置文件产生变更时,用该注解标注的办法会立即失去告诉。咱们来看下办法:

该注解波及到工夫对象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() 例子如下:

本文由传智教育博学谷 – 狂野架构师教研团队公布,转载请注明出处!

如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源

正文完
 0