Apollo 项目于 2016 年在携程框架研发部诞生,初衷是为了解决公司内部配置管理尤其是中间件公共配置的管理难题,秉持着开源开放的精神,项目从第一行代码开始就在开源社区上开源,可以说是一个完全开放的项目。
https://www.apolloconfig.com/#/zh/README
https://gitee.com/apolloconfig
git clone https://gitee.com/apolloconfig/apollo.git
git clone https://gitee.com/apolloconfig/apollo-use-cases.git
- Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端
- Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)
- Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳
- 在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口
- Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试
- Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试
- 为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中
docker环境搭建
git clone https://gitclone.com/github.com/apolloconfig/apollo-quick-start.git
cd ./apollo-quick-start
docker-compose -f docker-compose.yml up
Gracefully stopping… (press Ctrl+C again to force)
docker-compose -f docker-compose.yml start
docker-compose -f docker-compose.yml stop
docker-compose -f docker-compose.yml down -v
@Value("${base.url}")
private String url;
Spring 注解,支持热更新 (前提确保
apollo.autoUpdateInjectedSpringProperties=true
默认true)
key | value |
---|---|
student | {“name”:“zs”, “age”:30} |
@ApolloJsonValue("${student}")
private Student student;
Apollo内部注解,支持热更新 (前提确保
apollo.autoUpdateInjectedSpringProperties=true
默认true)
key | value |
---|---|
student.name | zs |
student.age | 30 |
@Data
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
private String name;
private Integer age;
}
spring boot注解,不支持热更新,需结合spring cloud注解
@RefreshScope
和Apollo注解@ApolloConfigChangeListener
自行实现更新逻辑。示例如下
@ApolloConfigChangeListener
注意:
此处
@ApolloConfigChangeListener(value = {"student.properties"}, interestedKeyPrefixes = "student.")
标注的属性配置类,不要增加@RefreshScope
注解;务必增加
@ApolloConfigChangeListener
value属性即namespace,否则onChange
方法可能监听不到事件源
@Data
@Component
@ConfigurationProperties(prefix = "student")
public class Student {
private String name;
private Integer age;
}
@Configuration
public class CustomPropertiesRefresher implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@ApolloConfigChangeListener(value = {"student.properties"}, interestedKeyPrefixes = "student.")
public void onChange(ConfigChangeEvent changeEvent) {
System.out.println(changeEvent);
/**
* @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
*/
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
}
}
@RefreshScope
+ @ApolloConfigChangeListener
@Data
@Component
@RefreshScope
@ConfigurationProperties(prefix = "student")
public class Student {
private String name;
private Integer age;
}
@Configuration
public class CustomPropertiesRefresher {
private final RefreshScope refreshScope;
public CustomPropertiesRefresher(RefreshScope refreshScope) {
this.refreshScope = refreshScope;
}
@ApolloConfigChangeListener(value = {"student.properties"}, interestedKeyPrefixes = "student.")
public void onChange(ConfigChangeEvent changeEvent) {
// 刷新spring bean实例(与此前的bean对象不是同一个)
refreshScope.refresh("student");
}
}
方式1 采用
EnvironmentChangeEvent
事件监听机制(观察者模式)扩展性较好;方式2 采用
@RefreshScope
重新初始化bean对象,较重,若仅刷新指定属性配置类,需要讲bean name写死(不推荐);但,方式2并非无用武之地,比如修改配置属性后,需要重新初始化bean实例(例如redis集群实例)的场景就很适合该方式。
@ApolloConfigChangeListener
监听的namespace也可在application.yml
中配置,多个namespace可使用逗号分割
apollo:
meta: http://81.68.181.139:8080
bootstrap:
enabled: true
eagerLoad:
enabled: true
# will inject 'application' and 'TEST1.apollo' namespaces in bootstrap phase
namespaces: application,TEST1.apollo,application.yaml
listeners: "application,TEST1.apollo,application.yaml"
@ApolloConfigChangeListener(value = "${listeners}", interestedKeyPrefixes = {"student."})
Apollo默认会在本地磁盘缓存配置文件,默认缓存目录为
/opt/data
;你可以通过操作系统的System Environment
APOLLO_CACHE_DIR
(1.9.0+) 或者APOLLO_CACHEDIR
(1.9.0之前)来指定默认缓存目录位置,也可以在Spring Boot的application.properties
或bootstrap.properties
中指定apollo.cache-dir=/opt/data/some-cache-dir
(1.9.0+) 或者apollo.cacheDir=/opt/data/some-cache-dir
(1.9.0之前)指定缓存目录位置;
对于数据库密码、appKey等敏感信息仍然存在泄露风险,可以根据需要考虑结合
jasypt
使用,需要注意的是,使用jasypt
加密的属性值热更新会失效,需要自行处理。
/opt/data/{appId}
C:\opt\data\{appId}
<dependency>
<groupId>com.github.ulisesbocchiogroupId>
<artifactId>jasypt-spring-boot-starterartifactId>
<version>3.0.4version>
dependency>
Apollo从1.6.0版本开始增加访问密钥机制,从而只有经过身份验证的客户端才能访问敏感配置。如果应用开启了访问密钥,客户端需要配置密钥,否则无法获取配置。
apollo.accesskey.secret=1cf998c4e2ad4704b45a98a509d15719
上图简要描述了Apollo客户端的实现原理:
apollo.refreshInterval
来覆盖,单位为分钟。注意:在本地开发模式下,Apollo不会实时监测文件内容是否有变化,所以如果修改了配置,需要重启应用生效。
=>
com\ctrip\framework\apollo\apollo-client\1.4.0\apollo-client-1.4.0.jar!\META-INF\spring.factories
=>
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer#initialize(org.springframework.context.ConfigurableApplicationContext)
=>
com.ctrip.framework.apollo.ConfigService#getConfig
=>
com.ctrip.framework.apollo.internals.RemoteConfigRepository#RemoteConfigRepository
// 启动时http get请求主动拉取数据 this.trySync(); // 创建单线程线程池定时拉取(频率默认5分钟) this.schedulePeriodicRefresh(); // 开启长轮询(客户端 默认延迟2秒执行、每秒限流2次、尝试获取令牌5秒超时后再等5秒、get请求超时时间90秒,服务端超时时间要小于90秒,现在是60秒,若超时则等待1~120秒每次乘2即1,2,4,8,16...96,120),长轮询本质也是pull模式,只不过无数据变更时,阻塞直至超时或有新数据产生(保证实时性要求的同时,优化无效查询降低服务端压力,嗯不错的思路有空我也试试) // com.ctrip.framework.apollo.internals.RemoteConfigLongPollService#startLongPolling this.scheduleLongPollingRefresh();
配置项
com.ctrip.framework.apollo.util.ConfigUtil
=>
com.ctrip.framework.apollo.internals.AbstractConfigRepository#trySync
=>
com.ctrip.framework.apollo.internals.AbstractConfigRepository#fireRepositoryChange
=>
com.ctrip.framework.apollo.internals.AbstractConfig#fireConfigChange
@Value
和@ApolloJsonValue
自动更新=>
com.ctrip.framework.apollo.spring.property.AutoUpdateConfigChangeListener#onChange
@Override public void onChange(ConfigChangeEvent changeEvent) { Set<String> keys = changeEvent.changedKeys(); if (CollectionUtils.isEmpty(keys)) { return; } for (String key : keys) { // 1. check whether the changed key is relevant Collection<SpringValue> targetValues = springValueRegistry.get(beanFactory, key); if (targetValues == null || targetValues.isEmpty()) { continue; } // 2. update the value for (SpringValue val : targetValues) { updateSpringValue(val); } } }
springValueRegistry
来源于注解扫描结果com.ctrip.framework.apollo.spring.annotation.ApolloProcessor
;该接口有两个实现类分别为
com.ctrip.framework.apollo.spring.annotation.SpringValueProcessor
和com.ctrip.framework.apollo.spring.annotation.ApolloJsonValueProcessor
分别对应@Value
和@ApolloJsonValue
注解
- 自定义listener更新
ConfigurationProperties
=>
com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor#processMethod
=>
com.example.demo.listener.CustomPropertiesRefresher#onChange
@ApolloConfigChangeListener(value = {"student.properties"}, interestedKeyPrefixes = "student.") public void onChange(ConfigChangeEvent changeEvent) { System.out.println(changeEvent); /** * @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent */ this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys())); }
=>
org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
=>
org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind(java.lang.String)