【知其然,知其所以然】配置中心 Apollo源码剖析()

  本篇文章为你整理了【知其然,知其所以然】配置中心 Apollo源码剖析()的详细内容,包含有 【知其然,知其所以然】配置中心 Apollo源码剖析,希望能帮助你了解 【知其然,知其所以然】配置中心 Apollo源码剖析。

  1 Apollo源码搭建

  在上一章我们已经学习了Apollo项目实战,为了更进一步学习Apollo、掌握Apollo工作原理,我们开始学习Apollo源码,所以我们先搭建Apollo源码环境。

  1.1 源码下载

  我们从github上 https://github.com/ctripcorp/apollo 下载源码,下载后的源码如下:

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

  1.2 导入数据库

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

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

  1.3 apollo-assembly启动服务

  我们启动Apollo服务,需要同时启动configservice、adminservice,如果手动启动比较慢,Apollo帮我们封装了一个工程apollo-assembly,可以基于该工程同时启动 apollo-adminservice 和 apollo-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中的两个参数分别表示启动configservice和adminservice服务。

  启动完成后,我们请求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 b0 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 {

   * 编号

   @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 字段,是否删除,用于逻辑删除的功能。

  dataChangeCreatedBy 和 dataChangeCreatedTime 字段,实现数据的创建人和时间的记录,方便追踪。

  dataChangeLastModifiedBy 和 dataChangeLastModifiedTime 字段,实现数据的更新人和时间的记录,方便追踪。

  @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("Applications 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 b0 COMMENT namespace是否为公共,

   `Comment` varchar(64) NOT NULL DEFAULT COMMENT 注释,

   `IsDeleted` bit(1) NOT NULL DEFAULT b0 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 b0 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-portal的com.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) 方。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: