第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-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 Service2: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
字段,是否删除,用于逻辑删除的性能。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()")@PostMappingpublic 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 数库。代码如下:
@Transactionalpublic 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是执行近程操作,代码如下:
@Componentpublic 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)
办法,代码如下:
@Transactionalpublic 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 对象到数据库中。代码如下:
@Transactionalpublic 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-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 数据库。代码如下:
@Transactionalpublic 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代码如下:
@EventListenerpublic 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 对象到数据库中。代码如下:
@Transactionalpublic 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 对象到数据库中。代码如下:
@Transactionalpublic 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 */@Overrideprotected 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()
例子如下:
本文由传智教育博学谷 - 狂野架构师教研团队公布,转载请注明出处!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源