第2章 Apollo源码剖析

能力目标

  • 能够基于Git导入Apollo源码
  • 能够基于IDEA实现DEBUG分析APP创建
  • 掌握Namespace创建过程
  • 掌握Item创建过程
  • 掌握灰度发布创建过程

1:namespace创建、灰度发布配置、Item创建作为自学

2:客户端剖析

​ 通信->Http、轮询机制

​ 配置文件优先级、缓存、关联关系

​ 刷新机制【注解解析】

1 Apollo源码搭建

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

1.1 源码下载

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

版本切换至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: truestatus-page-url-path: /infohealth-check-url-path: /healthserver:peerEurekaNodesUpdateIntervalMs: 60000enableSelfPreservation: falseclient:serviceUrl:# This setting will be overridden by eureka.service.url setting from ApolloConfigDB.ServerConfig or System Property# see com.ctrip.framework.apollo.biz.eureka.ApolloEurekaClientConfigdefaultZone: http://${eureka.instance.hostname}:8080/eureka/healthcheck:enabled: trueeurekaServiceUrlPollIntervalSeconds: 60fetch-registry: falseregister-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;/*** 保存前置方法*/@PrePersistprotected void prePersist() {if (this.dataChangeCreatedTime == null) {dataChangeCreatedTime = new Date();}if (this.dataChangeLastModifiedTime == null) {dataChangeLastModifiedTime = new Date();}}/*** 更新前置方法*/@PreUpdateprotected void preUpdate() {this.dataChangeLastModifiedTime = new Date();}/*** 删除前置方法*/@PreRemoveprotected 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.");}// Emailapp.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*/@EventListenerpublic 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);//protectionApp 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 RolenamespaceService.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 prefixApp app = appService.load(appId);if (app == null) {throw new BadRequestException("App not exist. AppId = " + appId);}// public namespaces only allow properties formatif (appNamespace.isPublic()) {appNamespace.setFormat(ConfigFileFormat.Properties.getValue());}// 拼接 AppNamespace 的 `name` 属性。StringBuilder appNamespaceName = new StringBuilder();//add prefix postfixappNamespaceName.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 namespaceif (appNamespace.isPublic()) {checkAppNamespaceGlobalUniqueness(appNamespace);} else {// 私有类型,校验 `name` 在 App 下唯一// check private app namespaceif (appNamespaceRepository.findByAppIdAndName(appNamespace.getAppId(), appNamespace.getName()) != null) {throw new BadRequestException("Private AppNamespace " + appNamespace.getName() + " already exists!");}// should not have the same with public app namespacecheckPublicAppNamespaceGlobalUniqueness(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());}//不存在,就添加AppNamespaceentity = 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);//protectionappNamespace.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 implementsApplicationContextInitializer<ConfigurableApplicationContext> , EnvironmentPostProcessor, Ordered {@Overridepublic void initialize(ConfigurableApplicationContext context) {//从ConfigurableApplicationContext获取EnvironmentConfigurableEnvironment 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 initializedreturn;}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();//获取要监听的keyString[] annotatedInterestedKeys = annotation.interestedKeys();//获取要监听的key的前缀集合String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes();//创建监听ConfigChangeListener configChangeListener = new ConfigChangeListener() {@Overridepublic 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设置listenerfor (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()例子如下:

本文由传智教育博学谷 - 狂野架构师教研团队发布 如果本文对您有帮助,欢迎关注和点赞;如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力 转载请注明出处!

【知其然,知其所以然】配置中心 Apollo源码剖析相关推荐

  1. Apollo源码剖析学习笔记2

    Apollo 源码剖析学习笔记2 Talker-ListenerNode 目录中包含了 Node 对象.Reader 对象和 Writer 对象.Node 对象主要对应 Ros 中的 Node 节点, ...

  2. 知行合一:知其然知其所以然

    知行合一:知其然知其所以然. 一个事情做成了,并不意味着你知道做成这件事背后蕴藏的原理. 只有使用冥思.事上练的方法了,将背后的原理提取出来,才能说做到了执行合一. 这个过程是行对知的精进过程. 转载 ...

  3. docker-compose 一键部署分布式配置中心Apollo

    简介 说起分布式肯定要想到分布式配置中心.分布式日志.分布式链路追踪等 在分布式部署中业务往往有很多配置比如: 应用程序在启动和运行时需要读取一些配置信息,配置基本上伴随着应用程序的整个生命周期,比如 ...

  4. apollo local 模式_「架构」 - 配置中心 Apollo基本使用

    公司需要使用Apollo配置中心,下面会出现大段Apollo官网文字截取引用. 一.Apollo(配置中心) Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境.不同 ...

  5. apollo @value没生效_不问不要紧,一文要人命,绝对的面试加分项配置中心Apollo深度解读...

    往期推荐 阿里面试官问我:到底知不知道什么是Eureka,这次,我没沉默 万字详解Ribbon架构,针对面试高频题多角度细说Ribbon 什么是Hystrix,阿里技术最终面,遗憾的倒在Hystrix ...

  6. 微服务 分布式配置中心Apollo详解

    微服务 分布式配置中心Apollo详解 1. 配置中心概述 1.1 配置中心简介 1.2 配置中心特点 1.3 配置中心对比 2. Apollo概述 2.1 Apollo简介 2.2 Apollo特点 ...

  7. JAVA开发与运维(配置中心Apollo的使用)

    在微服务构建的分布式系统,有一个组件很重要,就是配置中心.配置中心集中管理所有微服务的配置信息,这样做既规范了开发配置规范,也方便于后期的运维管理.这一篇我们讲一下携程的开源配置中心Apollo的使用 ...

  8. STL源码剖析 空间配置器 查漏补缺

    ptrdiff_t含义 减去两个指针的结果的带符号整数类型 ptrdiff_t (Type support) - C 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云 std::set_new_ ...

  9. Apollo 7.0——percception:lidar源码剖析(万字长文)

    文章目录 组件启动 实现组件类 实现组件头文件 实现组件源文件 设置配置文件 启动组件 激光感知 目录结构 源码剖析 detection--init InitAlgorithmPlugin detec ...

最新文章

  1. 赠书 | 干货!用 Python 动手学强化学习
  2. Office 365系列(4)------Cutover Migrate 搬迁方式至O365上来方法及步骤总结
  3. MySQL优化—工欲善其事,必先利其器之EXPLAIN
  4. adguard拦截规则存在哪里_AdGuard 过滤规则分享
  5. 【数据结构与算法】之深入解析“穿过迷宫的最少移动次数”的求解思路与算法示例
  6. Kubernetes 1.14重磅来袭,多项关键特性生产可用
  7. 交叉渡线道岔规格_交叉渡线铁路道岔的型号及选用
  8. qsort与sort
  9. Python下载中国数据库大会(DTCC2020)PPT全集
  10. 02--Tomcat总体结构分析一
  11. Libevent源码解析
  12. C语言程序设计(第三版)何钦铭著 习题2-6
  13. 易优CMS:arcview的基础用法
  14. 2020 全国省份数据整理
  15. 【琐识】日常获取知识随笔
  16. 如何制定切实可行的计划并好好执行——2020年,我不想再碌碌无为
  17. 报名 | 2019世界人工智能大会-腾讯论坛:8月,在上海等你!
  18. 设计模式_访问者模式
  19. 4G模块SIM7600绑定AT指定串口号(二)
  20. html5 css3在线工具,HTML5/CSS3开发辅助工具(TopStyle)

热门文章

  1. JavaSE HotSpot VM 8 垃圾收集调优指南-介绍(2)
  2. win10双显示器,不论设置1还是2为主显示器,背景中,右键只显示“为监视器2设置”?
  3. 手机端自适应遇到的问题 页面缩放不正常(使用的是flexible.js)
  4. 孙陶然:核心价值观是企业用人的“一票否决权”
  5. 如何提高自己的执行力?只需要这一个方法!
  6. 使用freenom注册免费顶级域名并在梅林上使用DDNS
  7. SpringBoot整合Mybatis-Plus入门案例
  8. 阿里云短信发送回执错误码
  9. SWIFT之殇——针对越南先锋银行的黑客攻击技术初探
  10. 实例分析!如何快速搭建OA办公系统