Apollo项目实战

能力目标

掌握Apollo安装

掌握Apollo配置操作

能够实现SpringBoot集成Apollo

能够实现Apollo适用功能【自动刷新、监听器、配置加密、1灰度发布】

Apollo应用

image-20220219150518448

初识Apollo

Apollo(阿波罗)是携程框架部门研发的==分布式配置中心==,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。

服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。

Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时对Spring/Spring Boot环境也有较好的支持。

1)核心功能:

  • 统一管理不同环境、不同集群配置

1:Apollo提供了一个统一界面集中式管理不同环境 (environment)、不同集群(cluster)、不同命名空间(namespace) 的配置。

2:同一份代码部署在不同的集群,可以有不同的配置,比如zk的地址等

3:通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许应用对共享的配置进行覆盖

4:配置界面支持多语言(中文,English)

  • 热发布:

用户在Apollo修改完配置并发布后,客户端能实时(1秒)接收到最新的配置,并通知到应用程序。

  • 版本发布管理:

所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。

  • 灰度发布:

支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后再推给所有应用实例。

  • 权限管理、发布审核、操作审计:

1:应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了 编辑和发布两个环节,从而减少人为的错误。

2:所有的操作都有审计日志,可以方便的追踪问题

  • 客户端配置信息监控:

可以方便的看到配置在被哪些实例使用

  • 提供Java和.Net原生客户端:

1:提供了Java和.Net的原生客户端,方便应用集成

2:支持Spring Placeholder,Annotation和Spring Boot的ConfigurationProperties,方便应用使用(需要Spring 3.1.1+)

3:同时提供了Http接口,非Java和.Net应用也可以方便的使用

  • 提供开放平台API:

1:Apollo自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、流程治理等特性。

2:不过Apollo出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够保存。

3:在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,如xml, json,需要对格式做校验。

4:还有一些使用方如DAL,不仅有特定的格式,而且对输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配。

5:对于这类应用,Apollo支持应用方通过开放接口在Apollo进行配置的修改和发布,并且具备完善的授权和权限控制

  • 部署简单:

1:配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少

2:目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让Apollo跑起来

3:Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

2)谁在用它

国内很多大厂都在用Apollo作为分布式配置中心,如果你们的网站也在用,在https://github.com/ctripcorp/apollo/issues/451可以进行登记。

携程、华为、京东、智联招聘、中国移动、360金融、中通快递、转转、贝壳网、土巴兔、平安银行、有赞等众多国内大厂在用Apollo。

image-20220219151232007

Apollo单机部署

我们接下来实现Apollo安装,安装前我们先介绍一下单机版装的架构,如下图:

image-20220219151258431

上图展示了Apollo单机部署架构,我们对其中每个节点进行解释说明:

  • Apollo Config Service:提供配置的读取、推送等功能,服务对象是Apollo 客户端。

  • Apollo Admin Service:提供配置的修改、发布等功能,服务对象是Apollo Portal。

  • Apollo Portal:Apollo 的管理界面,进行不同项目的配置(项目配置、权限配置等),服务对象是开发者和开放平台API。

环境说明:

  • MySQL:MySQL建议用5.7,版本要求5.6.5+

  • Apollo服务端:JDK1.8+

  • Apollo客户端:JDK1.7+

Apollo安装方式有多种,官方提供了快速安装模式和Docker安装模式,我们把两种安装模式都实现一次,但如果是生产环境请使用分布式部署方案
https://github.com/ctripcorp/apollo/wiki/%E5%88%86%E5%B8%83%E5%BC%8F%E9%83%A8%E7%BD%B2%E6%8C%87%E5%8D%97)

快速安装

1)JDK版本:

1
2
3
4
[root@skywalking ~]# java -version 
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

2)MySQL版本:

1
2
3
SELECT VERSION() 
-----------------
5.7.32-log

3)安装包下载:
Apollo已经准备好了一个Quick Start安装包 apollo-quick-start.zip ,里面包含了可以自动启动的jar包、以及所有依赖jar包、数据库脚本、内置Tomcat容器等,安装包共63M,大家只需要下载到本地,就可以直接使用,免去了编译、打包过程。

Github下载地址:https://github.com/nobodyiam/apollo-build-scripts

Quick Start只针对本地测试使用,所以一般用户不需要自己下载源码打包,只需要下载已经打好的包即可。不过也有部分用户希望在修改代码后重新打包,那么可以参考如下步骤:

1
2
3
4
5
1:修改apollo-configservice, apollo-adminservice和apollo-portal的pom.xml,注释掉spring-boot-maven-plugin和maven-assembly-plugin

2:在根目录下执行mvn clean package -pl apollo-assembly -am -DskipTests=true

3:复制apollo-assembly/target下的jar包,rename为apollo-all-in-one.jar

4)创建数据库:
Apollo服务端共需要两个数据库: ApolloPortalDB 和 ApolloConfigDB ,我们把数据库、表的创建和样例数据都分别准备了sql文件,只需要导入数据库即可。
注意:如果你本地已经创建过Apollo数据库,请注意备份数据。我们准备的sql文件会清空Apollo相关的表。

image-20220219151634875

5)配置数据库连接:
我们的数据库地址不是固定的,Apollo服务端需要知道如何连接到你前面创建的数据库,因此需要修改数据库连接地址,在安装包里有一个启动脚本demo.sh,修改ApolloPortalDB和ApolloConfigDB相关的数据库连接串信息:

1
2
3
4
5
6
7
8
# apollo config db info 
apollo_config_db_url="jdbc:mysql://192.168.200.129:3306/ApolloConfigDB? characterEncoding=utf8&serverTimezone=Asia/Shanghai"
apollo_config_db_username=root
apollo_config_db_password=root
# apollo portal db info
apollo_portal_db_url="jdbc:mysql://192.168.200.129:3306/ApolloPortalDB? characterEncoding=utf8&serverTimezone=Asia/Shanghai"
apollo_portal_db_username=root
apollo_portal_db_password=root

6)启动服务:

Quick Start脚本会在本地启动3个服务,分别使用8070, 8080, 8090端口,请确保这3个端口当前没有被使用,如果端口没有被使用,我们可以直接启动程序,启动程序执行 demo.sh 脚本即可,启动过程比较慢。

1
./demo.sh start

当看到如下输出后,就说明启动成功了!

1
2
3
4
5
6
7
8
9
10
11
12
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

启动完成后访问 http://192.168.200.129:8070/,可以看到Apollo配置界面,登录账号apollo,密码admin

image-20220219151844965

Docker容器安装

如果您对Docker非常熟悉,可以使用Docker的方式快速部署Apollo,从而快速的了解Apollo。确保docker-quick-start文件夹已经在本地存在,如果本地已经clone过Apollo的代码,则可以跳过此步骤。

在docker-quick-start目录下执行 docker-compose up ,第一次执行会触发下载镜像等操作,需要耐心等待一些时间。

搜索所有 apollo-quick-start 开头的日志,看到以下日志说明启动成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
apollo-quick-start | ==== starting service ==== 

apollo-quick-start | Service logging file is

./service/apollo-service.log

apollo-quick-start | Started [45]

apollo-quick-start | Waiting for config service

startup.......

apollo-quick-start | Config service started. You may

visit http://localhost:8080 for service status now!

apollo-quick-start | Waiting for admin service

startup......

apollo-quick-start | Admin service started

apollo-quick-start | ==== starting portal ====

apollo-quick-start | Portal logging file is

./portal/apollo-portal.log

apollo-quick-start | Started [254]

apollo-quick-start | Waiting for portal startup.......

apollo-quick-start | Portal started. You can visit

http://localhost:8070 now!

不过基于Docker安装需要注意一些问题:

1
2
3
1:数据库的端口映射为13306,所以如果希望在宿主机上访问数据库,可以通过localhost:13306,用户名是root,密码留空。

2:如要查看更多服务的日志,可以通过docker exec -it apollo-quick-start bash登录, 然后到/apollo-quick-start/service和/apollo-quick-start/portal下查看日志信息。

安装完成后访问http://192.168.200.129:8070/,可以看到Apollo配置界面

image-20220219152012585

基本使用

登录apollo,进行如下操作

创建项目

1、登录后界面如下:

image-20220219152111807

2、创建项目

image-20220219152124227

点击创建应用,会出现如下表单:

image-20220219152132576

创建应用参数说明:

  • 部门:选择应用所在的部门。部门数据来自 ApolloPortalDB 库的ServerConfig 表的 Key = organizations 对应的记录。

  • 应用 AppId:用来标识应用身份的唯一 id,格式为 string,需要和客户端 app.properties 中配置的 app.id 对应。

  • 应用名称:应用名,仅用于界面展示。

  • 应用负责人:默认具有项目管理员权限。

  • 项目管理员:可以创建 Namespace 和集群、分配用户权限。

创建完成后如下图所示:

image-20220219152206426

创建配置

我们在默认的 namespace 下创建配置信息,默认的 namespace 支持的是properties 形式的配置,即: k=v 结构的

1、将 hailtaxi-driver 项目的配置托管到 apollo 中,在 apollo 中创建配置

image-20220219152229314

2、创建好的配置还未发布,一定要发布

image-20220219152242581

3、将 hailtaxi-driver 项目的其余配置从 nacos 中转移到 apollo

image-20220219152251621

也就是说 hailtaixi-driver 项目中的所有配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# spring: 
# datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://127.0.0.1:3306/hailtaxi-driver?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
# username: root
# password: root
spring:
cloud:
# sentinel 控制台
sentinel:
transport:
port: 8720
dashboard: 192.168.213.130:8858

management:
endpoint:
health:
show-details: always


# seata配置
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_seata_group
enable-auto-data-source-proxy: true
use-jdk-proxy: false
excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
client:
rm:
async-commit-buffer-limit: 1000
report-retry-count: 5
table-meta-check-enable: false
report-success-enable: false
saga-branch-register-enable: false
lock:
retry-interval: 10
retry-times: 30
retry-policy-branch-rollback-on-conflict: true
tm:
degrade-check: false
degrade-check-period: 2000
degrade-check-allow-times: 10
commit-retry-count: 5
rollback-retry-count: 5
undo:
data-validation: true
log-serialization: jackson
log-table: undo_log
only-care-update-columns: true
log:
exceptionRate: 100
service:
vgroup-mapping:
my_seata_group: default
#grouplist:
#default: 192.168.213.130:8091
enable-degrade: false
disable-global-transaction: false
transport:
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
server-executor-thread-prefix: NettyServerBizHandler
share-boss-worker: false
client-selector-thread-prefix: NettyClientSelector
client-selector-thread-size: 1
client-worker-thread-prefix: NettyClientWorkerThread
worker-thread-size: default
boss-thread-size: 1
type: TCP
server: NIO
heartbeat: true
serialization: seata
compressor: none
enable-client-batch-send-request: true
registry:
type: nacos
nacos:
application: seata-server
server-addr: 192.168.213.130:8848
group : "SEATA_GROUP"
namespace: b24962bb-2277-40b2-a08d-a9da0053d135
username: "nacos"
password: "nacos"

注意:seata的 namespace

而 apollo 中默认私有的 application 的 namespce 中支持的是properties 形式的,故需要将 yaml 转换成 properties

可以使用在线转换工具:https://www.toyaml.com/index.html将转换完成的 properties 添加到 apollo 中,可以使用批量添加

image-20220219152415859

添加完成后,发布!!!

应用接入

1、在 hailtaxi-driver 项目中 添加 apollo 的客户端依赖

1
2
3
4
5
6
<!--ApolloClient-->
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.7.0</version>
</dependency>

2、在 bootstrap.yml 配置文件中添加启动配置

首先停用 nacos 配置中心

1
2
3
4
5
spring: 
cloud:
nacos:
config:
enabled: false

然后添加 apollo 的启动配置,

3、配置 appId ,可以在 META-INF/app.properties 中配置

1
2
#使用的 Apollo 的项目(应用)编号 
AppId app.id=hailtaxi-driver-config

也可在 applicaton.properties/bootstrap.yml 中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
# 启用apollo配置中心
app:
id: hailtaxi-driver-config
apollo:
meta: http://192.168.200.200:8080 # Apollo Meta Server 地址
cacheDir: /opt/data
bootstrap:
enabled: true #是否开启 Apollo 配置预加载功能。默认为 false。
eagerLoad:
enable: true #是否开启 Apollo 支持日志级别的加载时机。默认为 false。
# 指定 namespace
namespaces: application,shared-datasource,mydriver.yaml #使用的 Apollo 的命名空间,默认为 application,多个用逗号隔开

image-20220426081649661

4、使用 @EnableApolloConfig 开启 apollo 配置,通过 -Denv=dev 指定环境,不指定默认连接dev

5、启动项目,访问:http://localhost:18081/driver/appinfo

6、验证动态刷新: apollo 也是支持 Environment 和 @Value 刷新

在 apollo 中修改 app.name 和 app.version 查看动态刷新的结果

Apollo其他应用

公共配置

apollo 中的所有配置都有从属的 namespace ,而 namespace 有两种类型: public 和 private ,区别如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public: 

公共的Namespace的配置能被任何项目读取 通过创建公共Namespace可以实现公共组件的配置,或多个应用共享同一份 配置的需求

如果其它应用需要覆盖公共部分的配置,可以在其它应用那里关联公共 Namespace,然后在关联的Namespace里面配置需要覆盖的配置即可

如果其它应用不需要覆盖公共部分的配置,那么就不需要在其它应用那里关 联公共Namespace

private:

私有Namespace的配置只能被所属的应用获取到
通过创建一个私有的Namespace可以实现分组管理配置

私有Namespace的格式可以是xml、yml、yaml、json、txt. 您可以通过 apollo-client中ConfigFile接口来获取非properties格式Namespace 的内容

1.3.0及以上版本的apollo-client针对yaml/yml提供了更好的支持,可 以通过ConfigService.getConfig("someNamespace.yml")直接获取Config对象,也可以通过

@EnableApolloConfig("someNamespace.yml")或 apollo.bootstrap.namespaces=someNamespace.yml注入yml配置到 Spring/SpringBoot中去

配置创建

1、创建公共配置 shared-datasource

image-20220426082010463

公共 namespace 下的配置只能是 properties 类型的

2、假设现在数据源的配置作为公共配置,我们将数据源的配置添加到公共配置中

首先从默认的 application 中删除数据源的配置

image-20220426082025122

然后在 shared-datasource 下添加

1
2
3
4
5
6
7
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver 

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/hailtaxi-driver?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC

spring.datasource.username = root

spring.datasource.password = root

image-20220426082049661

注意:删除和添加都要在对应的 namespace 中提交!!!

3、在应用端( apollo 客户端)的 bootstrap.yml 中添加 namespace

1
2
3
4
5
6
7
8
9
10
11
# 启用apollo配置中心 
app:
id: hailtaxi-driver-config #使用的 Apollo 的项目(应用)编 AppId
apollo:
meta: http://192.168.200.129:8080 # Apollo Meta Server地址(它的注册中心地址,eureka地址)
bootstrap:
enabled: true #是否开启 Apollo 配置预加载功能。默认为false。
eagerLoad:
enable: true #是否开启 Apollo 支持日志级别的加载时机。默认为 false。
# 指定namespace
namespaces: application,shared-datasource #使用的 Apollo 的命名空间,默认为 application,多个用逗号隔开

4、启动测试:http://localhost:18081/driver/info/1

配置关联

我们接下来创建一个项目 hailtaxi-order ,在它里面关联公共配置

image-20220426082128713

然后创建 namespace 的时候关联公共配置

image-20220426082135743

完成后如下所示:

image-20220426082143926

当然如果对于有些公共配置,在本项目中我们还可以采取覆盖操作

image-20220426082150131

私有配置

私有 namespace 下的配置是可以在创建 namespace 时指定类型的.

1、在 hailtaxi-driver-config 项目中创建一个私有 namespace

image-20220426082159656

创建完项目下的展示如下

image-20220426082207336

2、在 mydriver 中添加如下配置

1
2
3
driver: 
name: 唐僧老师
age: 18

我们添加部分信息如下图:

image-20220426082227707

3、在 bootstrap.yml 中添加 namespace ,这里需要注意一下私有的namespace 名称如果是非 properties 的时,名字要带后缀

image-20220426082235620

4、找到 DriverController ,添加如下代码

1
2
3
4
5
6
7
8
@Value("${driver.name}") 
private String driverName;
@Value("${driver.age}")
private int driverAge;
@GetMapping("/driverinfo")
public String getDriverInfo() {
return driverName + ";"+driverAge;
}

5、启动,访问: http://localhost:18081/driver/driverinfo

配置刷新

@Value Environment

前面已经讲过, apollo 支持 @Value 和 Environment 动态刷新,而对于@ConfigurationProperties ,能加载到配置,但是无法自动刷新

1、创建 com.itheima.driver.properties.DriverProperties ,读取前面配置的 driver.name 和 driver.age 信息

1
2
3
4
5
6
7
@Configuration 
@ConfigurationProperties(prefix = "driver")
@Data
public class DriverProperties {
private String name;
private int age;
}

2、改造刚刚的

com.itheima.driver.controller.DriverController#getDriverInfo 方 法

1
2
3
4
5
6
7
@Autowired 
private DriverProperties driverProperties;

@GetMapping("/driverinfo")
public String getDriverInfo() {
return driverName + ";"+driverAge + ","+driverProperties;
}

3、启动访问:http://localhost:18081/driver/driverinfo

在apollo中修改配置,查看是否能刷新!!!

Apollo监听器

Apollo 已经能够满足我们绝大多数场景下的自动刷新配置的功能,但是项目中如果使用了 @ConfigurationProperties 此时只能借助Apollo的监听器功能实现数据刷新,可以先在监听器中读取变更的内容,然后调用指定的set方法执行重新赋值操作。

我们在 hailtaxi-driver 中创建监听器com.itheima.driver.listener.ApolloConfigListener ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.itheima.listener;

import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
@Slf4j
public class ApolloConfigListener implements ApplicationContextAware {

private ApplicationContext applicationContext;


@Autowired
RefreshScope refreshScope;

@ApolloConfigChangeListener(
value = {"application","shared-datasource","mydriver.yaml"}
,interestedKeyPrefixes = {"driver","app"}
)
public void onChange(ConfigChangeEvent event) {
Set<String> changedKeys = event.changedKeys();
for (String changedKey : changedKeys) {
String oldValue = event.getChange(changedKey).getOldValue();
String newValue = event.getChange(changedKey).getNewValue();
log.info("changedKey={},oldValue={},newValue={}",changedKey,oldValue,newValue);

}

applicationContext.publishEvent(new EnvironmentChangeEvent(event.changedKeys()));
refreshScope.refreshAll();
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

测试发现properties类型的配置能被监听到!!!

新建 test 名称空间(properties)类型,添加配置

1
2
driver.name = 黑马程序员唐僧老师!!! 
driver.age = 18

在apollo中修改配置,查看效果!!!

配置加密

考虑到安全性,我们可能最好将配置文件中的敏感信息进行加密。例如说,MySQL 的用户名密码、第三方平台的 Token 令牌等等。不过,Apollo 暂时未内置配置加密的功能。官方文档说明如下

image-20220426082552379

项目地址:https://github.com/ctripcorp/apollo-use-cases/tree/master/spring-boot-encrypt

1、在 hailtaxi-driver 中添加jasypt坐标依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--jasypt-->
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>

2、配置 jasypt 加密算法及对应的密钥(放到 bootstrap.yml 中即可)

1
2
3
4
5
6
7
8
9
jasypt:
encryptor:
#算法
algorithm: PBEWithMD5AndDES
#向量操作对象
iv-generator-classname: org.jasypt.iv.NoIvGenerator
#秘钥
password: itheima

配置数据可以放到apollo中,!!!

3、创建测试程序,将数据库密码加密:com.itheima.driver.DriverApplicationTest

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest 
@RunWith(SpringRunner.class)
public class DriverApplicationTest {
@Autowired
private StringEncryptor encryptor;
@Test
public void encode() {
String password = "root";
System.out.println(encryptor.encrypt(password));
}
}

测试运行输出的密文: 3OzWpNao6JPFAHYHeWIrlw==

4、将密文配置到apollo中

我们将密文配置到Apollo中,以 ENC(密文) 修饰,我们对数据连接中密码进行加密,如下图:

image-20220426082806262

5、启动项目进行测试:http://localhost:18081/driver/info/1

灰度发布

通过创建灰度版本,您可以对某些配置做灰度测试灰度流程为:

  • 1.创建灰度版本
  • 2.配置灰度配置项
  • 3.配置灰度规则.如果是私有的namespace可以按照客户端的IP进行灰度,如果是公共的namespace则可以同时按AppId和客户端的IP进行灰度
  • 4.灰度发布

灰度版本最终有两种结果:全量发布和放弃灰度

1、全量发布:灰度的配置合到主版本并发布,所有的客户端都会使用合并后的配置

2、放弃灰度:删除灰度版本,所有的客户端都会使用回主版本的配置

注意事项:如果灰度版本已经有灰度发布过,那么修改灰度规则后,无需再次灰度发布就立即生效

1、将 hailtaxi-driver 打包,上传到 192.168.200.129 服务器上,

2、在apollo中将`shared-datasource下将数据库的连接地址改一下,如下

image-20220426082859253

注意,数据库要开启远程访问权限!!!

3、在 129 机器上启动 hailtaxi-driver

1
java -jar hailtaxi-driver-1.0-SNAPSHOT.jar

4、针对 application 命名空间下的配置进行灰度

image-20220426082947908

按照 IP 进行灰度,灰度配置只在 129 机器上生效

如果 namespace 是 public 的,可以按照 appid 和 IP 进行灰度

5、灰度下修改配置,要点击灰度发布才生效!

image-20220426083001557

6、测试:

http://localhost:18081/driver/appinfo 还是原版本

http://192.168.200.129:18081/driver/appinfo 是灰度版本

6、全量灰度发布在现实工作中是指灰度版本没有问题了,需要把所有服务的版本全部切换成完成测试的灰度版本,我们点击全量发布即可,全量发布的时候,我们可以把灰度版本删除。

image-20220426083021793

7、再次测试:

http://localhost:18081/driver/appinfo 均是灰度后的版本了!!!

Apollo多环境配置

现实中Apollo是要作多环境部署的,比如: dev , test , pro 等等

Apollo多环境配置的要点是,每多配置一个环境就多启动两套服务(apollo-adminsevice+apollo-configservice),apollo-portal服务永远只启动一套,所以我们的步骤是下载好这三套服务,按照自己设定的每个环境把文件分布好、设置好端口、数据库等,然后启动

1、下载三个服务

1
2
3
4
5
wget https://github.com/ctripcorp/apollo/releases/download/v1.7.0/apollo-adminservice-1.7.0-github.zip 

wget https://github.com/ctripcorp/apollo/releases/download/v1.7.0/apollo-configservice-1.7.0-github.zip

wget https://github.com/ctripcorp/apollo/releases/download/v1.7.0/apollo-portal-1.7.0-github.zip

2、创建对应的目录并赋权限

1
2
3
4
5
6
7
8
9
10
11
mkdir -p adminservice/dev 

mkdir -p adminservice/pro

mkdir -p configservice/dev

mkdir -p configservice/pro

mkdir portal

chmod -R 755 adminservice/ configservice/ portal/

3、解压到对应的目录下

1
2
3
4
5
6
7
8
9
unzip apollo-adminservice-1.7.0-github.zip -d adminservice/dev 

unzip apollo-adminservice-1.7.0-github.zip -d adminservice/pro/

unzip apollo-configservice-1.7.0-github.zip -d configservice/dev

unzip apollo-configservice-1.7.0-github.zip -d configservice/pro/

unzip apollo-portal-1.7.0-github.zip -d portal

4、准备数据库,现在有两套环境, dev 和 pro 需要单独准备数据库

在 192.168.200.129 上创建两套库,,主要是针对 apolloconfigdb , apolloportaldb 只需要一份

image-20220426083227855

5、在数据库中修改相关参数信息

image-20220426083235128

在 apolloportaldb 中增加环境列表

image-20220426083241481

6、确保 apollo-configservice 服务和 apollo-adminservice 服务启动时连接各自的数据库

编辑

/adminservice/dev/config/application-github.properties

1
2
3
4
5
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloConfigDB_dev?characterEncoding=utf8 

spring.datasource.username = root

spring.datasource.password = root

/adminservice/pro/config/application-github.properties

1
2
3
4
5
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloConfigDB_pro?characterEncoding=utf8 

spring.datasource.username = root

spring.datasource.password = root

编辑

/configservice/dev/config/application-github.properties

1
2
3
4
5
6
7
# DataSource 

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloConfigDB_dev?characterEncoding=utf8

spring.datasource.username = root

spring.datasource.password = root

/configservice/pro/config/application-github.properties

1
2
3
4
5
6
7
# DataSource 

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloConfigDB_pro?characterEncoding=utf8

spring.datasource.username = root

spring.datasource.password = root

7、修改端口占用冲突,现在在一台主机上部署了两套 adminservice 和 configservice ,我们修改 pro 环境下这个两个服务占用的端口

/adminservice/pro/scripts/startup.sh

1
2
3
4
5
6
7
## Adjust log dir if necessary 

LOG_DIR=/opt/logs/100003174

## Adjust server port if necessary

SERVER_PORT=${SERVER_PORT:=8091}

注意端口冲突, seata 默认占用的端口也是8091

/configservice/pro/scripts/startup.sh

1
2
3
4
5
6
7
## Adjust log dir if necessary 

LOG_DIR=/opt/logs/100003173

## Adjust server port if necessary

SERVER_PORT=${SERVER_PORT:=8081}

8、配置 apollo-portal 支持多套环境配置以及连接的数据库

/portal/config/application-github.properties

1
2
3
4
5
6
7
# DataSource 

spring.datasource.url = jdbc:mysql://127.0.0.1:3306/ApolloPortalDB_all?characterEncoding=utf8

spring.datasource.username = root

spring.datasource.password = root

/portal/config/apollo-env.properties

1
2
3
4
5
6
7
8
9
10
11
local.meta=http://localhost:8080 

dev.meta=http://localhost:8080

fat.meta=http://fill-in-fat-meta-server:8080

uat.meta=http://fill-in-uat-meta-server:8080

lpt.meta=${lpt_meta}

pro.meta=http://localhost:8081

9、依次启动 apollo-configservice , apollo-adminservice , apollo-portal

1
2
3
4
5
6
7
8
9
/configservice/dev/scripts/startup.sh 

/configservice/pro/scripts/startup.sh

/adminservice/dev/scripts/startup.sh

/adminservice/pro/scripts/startup.sh

/portal/scripts/startup.sh

10、验证,

访问:http://192.168.200.129:8070

image-20220426083600927

11、应用接入时通过接入不同的 meta server 即可接入不同环境

1
2
apollo: 
meta: http://192.168.200.129:8080 #连接apollo meta server

课后拓展:

apollo的集群配置

Apollo源码剖析

能力目标

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

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

2:客户端剖析

通信->Http、轮询机制

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

刷新机制【注解解析】

Apollo源码搭建

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

源码下载

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

image-20220426084001373

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

image-20220426084007293

导入数据库

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

image-20220426084014786

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

image-20220426084020283

apollo-assembly启动服务

我们启动Apollo服务,需要同时启动configservice、adminservice,如果手动启动比较慢,Apollo帮我们封装了一个工程 apollo-assembly ,可以基于该工程同时启动 apollo-adminservice 和 apollo-configservice 项目。修改 apollo-configservice 的核心配置文件 bootstrap.yml 添加Eureka不注册Eureka数据也不获取Eureka数据,配置如下

image-20220426084041802

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

management:
health:
status:
order: DOWN, OUT_OF_SERVICE, UNKNOWN, UP

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

image-20220426084058427

这里的VM optins:

1
2
3
4
5
6
7
8
9
10
-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

或者可以直接给github.yaml设置默认值
# DataSource
spring.datasource.url = ${spring_datasource_urljdbc:mysql://localhost:3306/ApolloConfigDB?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC}
spring.datasource.username = ${spring_datasource_username:root}
spring.datasource.password = ${spring_datasource_password:root}

参数 Program arguments 中的两个参数分别表示启动 configservice 和 adminservice 服务。

启动完成后,我们请求Eureka http://localhost:8080/

image-20220426084123484

PortalService启动

apollo-portal工程需要单独启动,启动的时候我们也需要配置密码和日志输出文件,如下图:

image-20220426084132875

VM options配置如下:

1
2
3
4
5
6
7
-Dapollo_profile=github,auth 
-Ddev_meta=http://localhost:8080/
-Dserver.port=8070
-Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloPo rtalDB?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 效果如下:

image-20220426084149796

服务测试

我们可以先创建一个项目并且app.id=100004458,如下图

image-20220426084202884

image-20220426084206156

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

image-20220426084216865

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

image-20220426084228149

VM options参数配置如下:

1
2
3
-Denv=dev 

-Ddev_meta=http://localhost:8080

启动程序,我们输入username回车,可以看到对应数据,如下输出结果:

1
2
3
4
5
6
7
8
9
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: 张三

Portal创建APP

Apollo创建App的过程如果基于控制台操作是很简单的,但是Apollo是如何

实现的呢,我们接下来进行相关源码剖析。

image-20220426084310560

创建APP的流程如上图

  • 1:用户在后台执行创建app,会将请求发送到Portal Service
  • 2:Portal Service将数据保存到Portal DB中
  • 3:Portal Service同时将数据同步到Admin Service中,这个过程是异步 的
  • 4:Admin Service将数据保存到Config DB中

创建APP

创建APP由Portal Service执行,我们从它的JavaBean、Controller、

Service、Dao一步一步分析。

实体Bean

1)Table

APP对应的表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `app` (
`Id` int 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 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='应用表';

2)App(Bean)

在 apollo-common 项目中,com.ctrip.framework.apollo.common.entity.App ,继承 BaseEntity 抽象类,应用信息实体。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package com.ctrip.framework.apollo.common.entity;

import com.ctrip.framework.apollo.common.utils.InputValidator;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

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

@NotBlank(message = "Name cannot be blank")
@Column(name = "Name", nullable = false)
private String name;

@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;

@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;

public String getAppId() {
return appId;
}

public String getName() {
return name;
}

public String getOrgId() {
return orgId;
}

public String getOrgName() {
return orgName;
}

public String getOwnerEmail() {
return ownerEmail;
}

public String getOwnerName() {
return ownerName;
}

public void setAppId(String appId) {
this.appId = appId;
}

public void setName(String name) {
this.name = name;
}

public void setOrgId(String orgId) {
this.orgId = orgId;
}

public void setOrgName(String orgName) {
this.orgName = orgName;
}

public void setOwnerEmail(String ownerEmail) {
this.ownerEmail = ownerEmail;
}

public void setOwnerName(String ownerName) {
this.ownerName = ownerName;
}

public String toString() {
return toStringHelper().add("name", name).add("appId", appId)
.add("orgId", orgId)
.add("orgName", orgName)
.add("ownerName", ownerName)
.add("ownerEmail", ownerEmail).toString();
}

public static class Builder {

public Builder() {
}

private App app = new App();

public Builder name(String name) {
app.setName(name);
return this;
}

public Builder appId(String appId) {
app.setAppId(appId);
return this;
}

public Builder orgId(String orgId) {
app.setOrgId(orgId);
return this;
}

public Builder orgName(String orgName) {
app.setOrgName(orgName);
return this;
}

public Builder ownerName(String ownerName) {
app.setOwnerName(ownerName);
return this;
}

public Builder ownerEmail(String ownerEmail) {
app.setOwnerEmail(ownerEmail);
return this;
}

public App build() {
return app;
}

}

public static Builder builder() {
return new Builder();
}


}

  • ORM 选用 Hibernate 框架。
  • @SQLDelete(...) + @Where(...) 注解,配合BaseEntity.extends 字段,实现 App 的逻辑删除
  • 字段比较简单。

3)BaseEntity(Bean)

com.ctrip.framework.apollo.common.entity.BaseEntity ,是基础实体抽象类。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
package com.ctrip.framework.apollo.common.entity;

import com.google.common.base.MoreObjects;
import com.google.common.base.MoreObjects.ToStringHelper;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;

@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;

@Column(name = "DataChange_CreatedBy", nullable = false)
private String dataChangeCreatedBy;

@Column(name = "DataChange_CreatedTime", nullable = false)
private Date dataChangeCreatedTime;

@Column(name = "DataChange_LastModifiedBy")
private String dataChangeLastModifiedBy;

@Column(name = "DataChange_LastTime")
private Date dataChangeLastModifiedTime;

public String getDataChangeCreatedBy() {
return dataChangeCreatedBy;
}

public Date getDataChangeCreatedTime() {
return dataChangeCreatedTime;
}

public String getDataChangeLastModifiedBy() {
return dataChangeLastModifiedBy;
}

public Date getDataChangeLastModifiedTime() {
return dataChangeLastModifiedTime;
}

public long getId() {
return id;
}

public boolean isDeleted() {
return isDeleted;
}

public void setDataChangeCreatedBy(String dataChangeCreatedBy) {
this.dataChangeCreatedBy = dataChangeCreatedBy;
}

public void setDataChangeCreatedTime(Date dataChangeCreatedTime) {
this.dataChangeCreatedTime = dataChangeCreatedTime;
}

public void setDataChangeLastModifiedBy(String dataChangeLastModifiedBy) {
this.dataChangeLastModifiedBy = dataChangeLastModifiedBy;
}

public void setDataChangeLastModifiedTime(Date dataChangeLastModifiedTime) {
this.dataChangeLastModifiedTime = dataChangeLastModifiedTime;
}

public void setDeleted(boolean deleted) {
isDeleted = deleted;
}

public void setId(long id) {
this.id = id;
}

@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();
}

protected ToStringHelper toStringHelper() {
return MoreObjects.toStringHelper(this).omitNullValues().add("id", id)
.add("dataChangeCreatedBy", dataChangeCreatedBy)
.add("dataChangeCreatedTime", dataChangeCreatedTime)
.add("dataChangeLastModifiedBy", dataChangeLastModifiedBy)
.add("dataChangeLastModifiedTime", dataChangeLastModifiedTime);
}

public String toString(){
return toStringHelper().toString();
}
}

部分注解和方法我们说明一下:

  • id 字段,编号,Long 型,全局自增。
  • isDeleted 字段,是否删除,用于逻辑删除的功能。
  • dataChangeCreatedBy 和 dataChangeCreatedTime 字段,实现数据的创建人和时间的记录,方便追踪。
  • dataChangeLastModifiedBy 和 dataChangeLastModifiedTime 字段,实现数据的更新人和时间的记录,方便追踪。
  • @PrePersist 、 @PreUpdate 、 @PreRemove 注解,CRD 操作前,设置对应的时间字段
  • 在 Apollo 中,所有实体都会继承 BaseEntity ,实现公用字段的统一定义。这种设计值得借鉴,特别是创建时间和更新时间这两个字段,特别适合线上追踪问题和数据同步。

数据为什么要同步呢?

在文初的流程图中,我们看到 App 创建时,在 Portal Service 存储完成后,会异步同步到 Admin Service 中,这是为什么呢?

在 Apollo 的架构中,一个环境( Env ) 对应一套 Admin Service 和 ConfigService 。

而 Portal Service 会管理所有环境( Env ) 。因此,每次创建 App 后,需要进行同步。

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

业务执行流程

1)Controller

在 apollo-portal 项目中,

com.ctrip.framework.apollo.portal.controller.AppController ,提供 App 的 API

创建项目的界面中,点击【提交】按钮,调用创建 App API

image-20220426084527833

处理请求的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 创建app(应用)
* @param appModel
* @return
*/
@PreAuthorize(value = "@permissionValidator.hasCreateApplicationPermission()")
@PostMapping
public App create(@Valid @RequestBody AppModel appModel) {
// 得到app
App app = transformToApp(appModel);
// apolloportaldb 库中添加app信息
App createdApp = appService.createAppInLocal(app);

// 发布app create 事件 同步数据到 apolloconfigdb
publisher.publishEvent(new AppCreationEvent(createdApp));

Set<String> admins = appModel.getAdmins();
if (!CollectionUtils.isEmpty(admins)) {
rolePermissionService
.assignRoleToUsers(RoleUtils.buildAppMasterRoleName(createdApp.getAppId()),
admins, userInfoHolder.getUser().getUserId());
}

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 数库。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Transactional
public App createAppInLocal(App app) {
String appId = app.getAppId();
// 先查询
App managedApp = appRepository.findByAppId(appId);

if (managedApp != null) {
throw new BadRequestException(String.format("App already exists. AppId = %s", appId));
}
// 获取 ownerName
UserInfo owner = userService.findByUserId(app.getOwnerName());
if (owner == null) {
throw new BadRequestException("Application's owner not exist.");
}
app.setOwnerEmail(owner.getEmail());
// 操作人
String operator = userInfoHolder.getUser().getUserId();
app.setDataChangeCreatedBy(operator);
app.setDataChangeLastModifiedBy(operator);
// 保存 app
App createdApp = appRepository.save(app);

// 在 应用下创建默认 namespace(application)
appNamespaceService.createDefaultAppNamespace(appId);
roleInitializationService.initAppRoles(createdApp);

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 。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 实现。

数据同步

在前面流程图中我们说过会调用Admin Service执行同步,同步过程是如何同步的呢,其实这里采用了观察者模式进行了监听操作,我们一起来分析一下。

观察者模式

定义:

对象之间存在一对多或者一对一依赖,当一个对象改变状态,依赖它的对象会 收到通知并自动更新。

MQ其实就属于一种观察者模式,发布者发布信息,订阅者获取信息,订阅了 就能收到信息,没订阅就收不到信息。

优点:

1:观察者和被观察者是抽象耦合的。

2:建立一套触发机制。

缺点:

1:如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察 者都通知到会花费很多时间。

2:如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间 进行循环调用,可能导致系统崩溃。

Spring观察者模式

ApplicationContext 事件机制是观察者设计模式的实现,通过ApplicationEvent 类和 ApplicationListener 接口,可以实现ApplicationContext 事件处理。

如果容器中有一个 ApplicationListener Bean ,每当ApplicationContext 发布 ApplicationEvent 时, ApplicationListener Bean 将自动被触发。这种事件机制都必须需要程序显示的触发。其中spring有一些内置的事件,当完成某种操作时会发出某些事件动作。

比如监听 ContextRefreshedEvent 事件,当所有的bean都初始化完成并被成功装载后会触发该事件,实现ApplicationListener<ContextRefreshedEvent> 接口可以收到监听动作,然后可以写自己的逻辑。

同样事件可以自定义、监听也可以自定义,完全根据自己的业务逻辑来处理。

事件监听

在Portal Service创建APP的controller中会创建时间监听,代码如下

image-20220426084817949

事件监听创建后,Portal Service中有一个监听创建监听对象,在该监听对象中会监听创建事件信息,并根据创建的APP进行同步调用,主要调用的是AppAPI,而AppAPI是执行远程操作,代码如下:

1

AppAPI使用了RestTemplate执行远程操作,代码如下:

image-20220426084832120

同步业务执行流程

在 apollo-adminservice 项目中,com.ctrip.framework.apollo.adminservice.controller.AppController ,提供 App 的 API

#create(AppDTO) 方法,创建 App 。代码如下:

1

com.ctrip.framework.apollo.biz.service.AdminService ,

#createNewApp(App) 方法,代码如下:

1

在 apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.AppService ,提供 App 的Service 逻辑给 Admin Service 和 Config Service 。

#save(App) 方法,保存 App 对象到数据库中。代码如下

1

至于Dao还是JPA操作,我们不再过多讲解了。

Namespace创建

image-20220426085002746

namespace创建的流程也是先经过Portal Service,再同步到Admin Service中,执行流程我们先来一起分析一下:

image-20220426085013327

这里我们发现有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 的某些配置。

我们接下来对该执行流程的源码进行剖析。

创建AppNamespace

AppNamespace创建由Portal Service发起,我们先来分析该工程。

实体Bean

1)Table

AppNamespace对应表表结构如下:

1

Namespace表结构如下:

1

2)实体Bean

在 apollo-common 项目中,com.ctrip.framework.apollo.common.entity.AppNamespace ,继承BaseEntity 抽象类,App Namespace 实体。代码如下:

1

  • 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 实体,是配置项的集合,类似于一个配置文件的概念。代码如下:

1

业务执行流程

1)Controller

提交业务请求会调用 apollo-portal 的 com.ctrip.framework.apollo.portal.controller.NamespaceControlle r ,Portal Service提供了提供 AppNamespace 和 Namespace 的 API 。 com.ctrip.framework.apollo.portal.controller.NamespaceContro ller 创建AppNamespace方法源码如下:

1

在这里我们不难发现它又创建了监听,所以肯定也会涉及数据同步。

2)Service

在 apollo-portal 项目中,com.ctrip.framework.apollo.portal.service.AppNamespaceService ,提供 AppNamespace 的 Service 逻辑。

#createAppNamespaceInLocal(AppNamespace) 方法,保存AppNamespace 对象到 Portal DB 数据库。代码如下:

1

关于Dao我们就不做分析了。

数据同步

事件监听

com.ctrip.framework.apollo.portal.listener.CreationListener ,对象创建监听器,目前监听 AppCreationEvent 和AppNamespaceCreationEvent 事件。

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

1

上面监听仍然会调用远程服务,使用了namespaceAPI执行了远程调用,部分源码如下

image-20220426085404221

同步业务执行流程

1)Controller

在 apollo-adminservice 项目中,com.ctrip.framework.apollo.adminservice.controller.AppNamespaceC ontroller ,提供 AppNamespace 的 API

#create(AppNamespaceDTO) 方法,创建 AppNamespace 。代码如下:

1

2)Service

在 apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.AppNamespaceService ,提供 AppNamespace 的 Service 逻辑给 Admin Service 和 Config Service 。

#save(AppNamespace) 方法,保存 AppNamespace 对象到数据库中。代码如下:

1

调用 #instanceOfAppNamespaceInAllCluster(appId, namespaceName, createBy) 方法,创建 AppNamespace 在 App 下,每个Cluster 的 Namespace 对象。代码如下:

image-20220426111522492

注意这里每次都调用了namespaceService.save()方法,该方法会保存Namespace。

在 apollo-biz 项目中,com.ctrip.framework.apollo.biz.service.NamespaceService ,提供Namespace 的 Service 逻辑给 Admin Service 和 Config Service 。

#save(Namespace) 方法,保存 Namespace 对象到数据库中。代码如下:

1

Apollo客户端

我们接下来分析一下Apollo客户端是如何获取Apollo配置信息的。

Spring扩展

我们要想实现Apollo和Spring无缝整合,需要在Spring容器刷新之前,从Apollo服务器拉取配置文件,并注入到Spring容器指定变量中,此时可以利用ApplicationContextInitializer 对象。

ConfigurableApplicationContext:可以操作配置文件信息,代码如下:

1

ApplicationContextInitializer 是Spring框架原有的东西,这个类的主要作用就是在 ConfigurableApplicationContext 类型(或者子类型)的 ApplicationContext 做refresh之前,允许我们对ConfiurableApplicationContext 的实例做进一步的设置和处理。ApplicationContextInitializer:代码如下

1
2
3
4
5
6
7
public interface ApplicationContextInitializer<C extends 
ConfigurableApplicationContext> {
/**
* 容器刷新之前调用该放啊
*/
void initialize(C applicationContext);
}

Apollo扩展Spring

Apollo利用Spring扩展机制实现了先从Apollo加载配置,并解析配置,再将数据添加到ConfigurableApplicationContext中,从而实现配置有限加载:

1

数据同步

1

@ApolloConfigChangeListener

@ApolloConfigChangeListener 注解是监听注解,当Apollo配置文件发生变更时,用该注解标注的方法会立刻得到通知。我们来看下方法:

image-20220426111734826

该注解涉及到时间对象 ConfigChangeEvent ,该对象信息如下:

1
2
3
4
5
6
7
8
9
10
11
public class ConfigChangeEvent { 

//命名空间

private final String m_namespace;

//变更数据

private final Map<String, ConfigChange> m_changes;

}

上面变更数据用到了一个对象记录 ConfigChange ,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ConfigChange { 

//命名空间

private final String namespace;

//属性名字

private final String propertyName;

//原始值

private String oldValue;

//新值

private String newValue;

//操作类型

private PropertyChangeType changeType;

}

1)监听器添加

ApolloAnnotationProcessor前置拦截器,为每个namespace添加监听器:

1

2)监听器执行

监听器执行在执行同步发现数据变更的时候执行,其中RemoteConfigRepository.sync()