Apollo 源码解析 —— 服务自身配置 ServerConfig
摘要: 原创出处 http://www.iocoder.cn/Apollo/server-config/ 「芋道源码」欢迎转载,保留摘要,谢谢!
- 1. 概述
- 2. ServerConfig
- 2.1 Portal 侧
- 2.2 Config 侧
- 3. RefreshablePropertySource
- 3.1 PortalDBPropertySource
- 3.2 BizDBPropertySource
- 4. RefreshableConfig
- 4.1 构造方法
- 4.2 初始化
- 4.3 获得值
- 4.4 PortalConfig
- 4.5 BizConfig
- 5. ServerConfigController
- 666. 彩蛋
1. 概述
老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》
Portal、Config Service、Admin Service 等等服务,自身需要配置服务。一种实现是,基于配置文件,简单方便。但是,不方便统一管理和共享。因此,Apollo 基于数据库实现类配置表 ServerConfig 。
老艿艿:如果胖友的系统暂时没有使用配置中心,
- 可以基于数据库实现类配置表 ServerConfig ,实现业务系统里面的配置功能,短平快。
- 配合 Redis 的 PUB/SUB 特性,实现配置更新的实时通知。
本文涉及的类如下图所示:
类图
2. ServerConfig
2.1 Portal 侧
在 apollo-portal
项目中,com.ctrip.framework.apollo.portal.entity.po.ServerConfig
,继承 BaseEntity 抽象类,ServerConfig 实体,服务器 KV 配置项。代码如下:
@Entity
@Table(name = "ServerConfig")
@SQLDelete(sql = "Update ServerConfig set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class ServerConfig extends BaseEntity {
/**
* KEY
*/
@Column(name = "Key", nullable = false)
private String key;
/**
* VALUE
*/
@Column(name = "Value", nullable = false)
private String value;
/**
* 备注
*/
@Column(name = "Comment", nullable = false)
private String comment;
}
- KV 结构,一个配置项,一条记录。例如:
2.2 Config 侧
在 apollo-biz
项目中,com.ctrip.framework.apollo.biz.entity.ServerConfig
,继承 BaseEntity 抽象类,ServerConfig 实体,服务器 KV 配置项。代码如下:
@Entity
@Table(name = "ServerConfig")
@SQLDelete(sql = "Update ServerConfig set isDeleted = 1 where id = ?")
@Where(clause = "isDeleted = 0")
public class ServerConfig extends BaseEntity {
/**
* KEY
*/
@Column(name = "Key", nullable = false)
private String key;
/**
* Cluster 名
*/
@Column(name = "Cluster", nullable = false)
private String cluster;
/**
* VALUE
*/
@Column(name = "Value", nullable = false)
private String value;
/**
* 备注
*/
@Column(name = "Comment", nullable = false)
private String comment;
}
提供给 Config Service、Admin Service 服务使用。
相比多了
cluster
属性,用于多机房部署使用。官方说明如下:在多机房部署时,往往希望 config service 和 admin service 只向同机房的 eureka 注册,要实现这个效果,需要利用 ServerConfig 表中的
cluster
字段。config service 和 admin service 会读取所在机器的
/opt/settings/server.properties
(Mac/Linux)或C:\opt\settings\server.properties
(Windows)中的idc
属性,如果该 idc 有对应的eureka.service.url
配置,那么就会向该机房的 eureka 注册 。- 默认情况下,使用
"default"
集群。
- 默认情况下,使用
KV 结构,一个配置项,一条记录。例如:
3. RefreshablePropertySource
com.ctrip.framework.apollo.common.config.RefreshablePropertySource
,实现 org.springframework.core.env.MapPropertySource
类,可刷新的 PropertySource 抽象类。代码如下:
public abstract class RefreshablePropertySource extends MapPropertySource {
public RefreshablePropertySource(String name, Map<String, Object> source) {
super(name, source);
}
@Override
public Object getProperty(String name) {
return this.source.get(name);
}
/**
* refresh property
*/
protected abstract void refresh();
}
- Spring PropertySource 体系,在 《【Spring4揭秘 基础2】PropertySource和Enviroment》 中,有详细解析。
#refresh()
抽象方法,刷新配置。
3.1 PortalDBPropertySource
com.ctrip.framework.apollo.portal.service.PortalDBPropertySource
,实现 RefreshablePropertySource 抽象类,基于 PortalDB的 ServerConfig 的 PropertySource 实现类。代码如下:
@Component
public class PortalDBPropertySource extends RefreshablePropertySource {
private static final Logger logger = LoggerFactory.getLogger(PortalDBPropertySource.class);
@Autowired
private ServerConfigRepository serverConfigRepository;
public PortalDBPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
public PortalDBPropertySource() {
super("DBConfig", Maps.newConcurrentMap());
}
@Override
protected void refresh() {
// 获得所有的 ServerConfig 记录
Iterable<ServerConfig> dbConfigs = serverConfigRepository.findAll();
// 缓存,更新到属性源
for (ServerConfig config : dbConfigs) {
String key = config.getKey();
Object value = config.getValue();
// 打印日志
if (this.source.isEmpty()) {
logger.info("Load config from DB : {} = {}", key, value);
} else if (!Objects.equals(this.source.get(key), value)) {
logger.info("Load config from DB : {} = {}. Old value = {}", key, value, this.source.get(key));
}
// 更新到属性源
this.source.put(key, value);
}
}
}
- 在 PortalDBPropertySource 构造方法中,我们可以看到,属性源的名字为
"DBConfig"
,属性源使用 ConcurrentMap 。 #refresh()
实现方法,从 PortalDB 中,读取所有的 ServerConfig 记录,更新到属性源source
。
3.2 BizDBPropertySource
com.ctrip.framework.apollo.biz.service.BizDBPropertySource
,实现 RefreshablePropertySource 抽象类,基于 ConfigDB 的 ServerConfig 的 PropertySource 实现类。代码如下:
@Component
public class BizDBPropertySource extends RefreshablePropertySource {
private static final Logger logger = LoggerFactory.getLogger(BizDBPropertySource.class);
@Autowired
private ServerConfigRepository serverConfigRepository;
public BizDBPropertySource(String name, Map<String, Object> source) {
super(name, source);
}
public BizDBPropertySource() {
super("DBConfig", Maps.newConcurrentMap());
}
String getCurrentDataCenter() {
return Foundation.server().getDataCenter();
}
@Override
protected void refresh() {
// 获得所有的 ServerConfig 记录
Iterable<ServerConfig> dbConfigs = serverConfigRepository.findAll();
// 创建配置 Map ,将匹配的 Cluster 的 ServerConfig 添加到其中
Map<String, Object> newConfigs = Maps.newHashMap();
// 匹配默认的 Cluster
// default cluster's configs
for (ServerConfig config : dbConfigs) {
if (Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, config.getCluster())) {
newConfigs.put(config.getKey(), config.getValue());
}
}
// 匹配数据中心的 Cluster
// data center's configs
String dataCenter = getCurrentDataCenter();
for (ServerConfig config : dbConfigs) {
if (Objects.equals(dataCenter, config.getCluster())) {
newConfigs.put(config.getKey(), config.getValue());
}
}
// 匹配 JVM 启动参数的 Cluster
// cluster's config
if (!Strings.isNullOrEmpty(System.getProperty(ConfigConsts.APOLLO_CLUSTER_KEY))) { // -Dapollo.cluster=xxxx
String cluster = System.getProperty(ConfigConsts.APOLLO_CLUSTER_KEY);
for (ServerConfig config : dbConfigs) {
if (Objects.equals(cluster, config.getCluster())) {
newConfigs.put(config.getKey(), config.getValue());
}
}
}
// 缓存,更新到属性源
// put to environment
for (Map.Entry<String, Object> config : newConfigs.entrySet()) {
String key = config.getKey();
Object value = config.getValue();
// 打印日志
if (this.source.get(key) == null) {
logger.info("Load config from DB : {} = {}", key, value);
} else if (!Objects.equals(this.source.get(key), value)) {
logger.info("Load config from DB : {} = {}. Old value = {}", key,
value, this.source.get(key));
}
// 更新到属性源
this.source.put(key, value);
}
}
}
- 提供给 Config Service、Admin Service 服务使用。
- 相比 PortalDBPropertySource ,BizDBPropertySource 多了多机房部署的 Cluster 过滤。在
#refresh()
实现方法中,按照默认 的 Cluster、数据中心的 Cluster、JVM 启动参数的 Cluster ,逐个匹配 ServerConfig 的cluster
字段。若匹配,最终会更新到属性源。 - 另外,使用 Foundation 类,获取数据中心的代码实现,我们后续单独分享。
4. RefreshableConfig
com.ctrip.framework.apollo.common.config.RefreshableConfig
,可刷新的配置抽象类。
4.1 构造方法
private static final Logger logger = LoggerFactory.getLogger(RefreshableConfig.class);
private static final String LIST_SEPARATOR = ",";
protected Splitter splitter = Splitter.on(LIST_SEPARATOR).omitEmptyStrings().trimResults();
/**
* RefreshablePropertySource 刷新频率,单位:秒
*/
//TimeUnit: second
private static final int CONFIG_REFRESH_INTERVAL = 60;
/**
* Spring ConfigurableEnvironment 对象
*/
@Autowired
private ConfigurableEnvironment environment;
/**
* RefreshablePropertySource 数组,通过 {@link #getRefreshablePropertySources} 获得
*/
private List<RefreshablePropertySource> propertySources;
/**
* register refreshable property source.
* Notice: The front property source has higher priority.
*/
propertySources
属性,RefreshablePropertySource 数组。- BizConfig 和 PortalConfig 实现该方法,返回其对应的 RefreshablePropertySource 实现类的对象的数组。
在
#setup()
初始化方法中,将自己添加到environment
中。通过
#getRefreshablePropertySources()
抽象方法,返回需要注册的 RefreshablePropertySource 数组。代码如下:protected abstract List<RefreshablePropertySource> getRefreshablePropertySources();
environment
属性,Spring ConfigurableEnvironment 对象。其 PropertySource 不仅仅包括propertySources
,还包括yaml
properties
等 PropertySource 。这就是为什么 ServerConfig 被封装成 PropertySource 的原因。CONFIG_REFRESH_INTERVAL
静态属性,每 60 秒,刷新一次propertySources
配置。
4.2 初始化
#setup()
方法,通过 Spring 调用,初始化定时刷新配置任务。代码如下:
1: @PostConstruct
2: public void setup() {
3: // 获得 RefreshablePropertySource 数组
4: propertySources = getRefreshablePropertySources();
5: if (CollectionUtils.isEmpty(propertySources)) {
6: throw new IllegalStateException("Property sources can not be empty.");
7: }
8:
9: // add property source to environment
10: for (RefreshablePropertySource propertySource : propertySources) {
11: propertySource.refresh();
12: environment.getPropertySources().addLast(propertySource);
13: }
14:
15: // 创建 ScheduledExecutorService 对象
16: // task to update configs
17: ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("ConfigRefresher", true));
18: // 提交定时任务,每分钟刷新一次 RefreshablePropertySource 数组
19: executorService.scheduleWithFixedDelay(() -> {
20: try {
21: propertySources.forEach(RefreshablePropertySource::refresh);
22: } catch (Throwable t) {
23: logger.error("Refresh configs failed.", t);
24: Tracer.logError("Refresh configs failed.", t);
25: }
26: }, CONFIG_REFRESH_INTERVAL, CONFIG_REFRESH_INTERVAL, TimeUnit.SECONDS);
27: }
- 第 3 至 7 行:调用
#getRefreshablePropertySources()
方法,获得 RefreshablePropertySource 数组。 - 第 9 至 13 行:循环调用
ConfigurableEnvironment#getPropertySources()#addLast(propertySource)
方法,将propertySources
注册到environment
中。 - 第 17 行:创建 ScheduledExecutorService 对象。
- 第 18 至 26 行:提交定时任务,每 60 秒,循环调用
RefreshablePropertySource#refresh()
方法,刷新propertySources
的配置。
4.3 获得值
public int getIntProperty(String key, int defaultValue) {
try {
String value = getValue(key);
return value == null ? defaultValue : Integer.parseInt(value);
} catch (Throwable e) {
Tracer.logError("Get int property failed.", e);
return defaultValue;
}
}
public boolean getBooleanProperty(String key, boolean defaultValue) {
try {
String value = getValue(key);
return value == null ? defaultValue : "true".equals(value);
} catch (Throwable e) {
Tracer.logError("Get boolean property failed.", e);
return defaultValue;
}
}
public String[] getArrayProperty(String key, String[] defaultValue) {
try {
String value = getValue(key);
return Strings.isNullOrEmpty(value) ? defaultValue : value.split(LIST_SEPARATOR);
} catch (Throwable e) {
Tracer.logError("Get array property failed.", e);
return defaultValue;
}
}
public String getValue(String key, String defaultValue) {
try {
return environment.getProperty(key, defaultValue);
} catch (Throwable e) {
Tracer.logError("Get value failed.", e);
return defaultValue;
}
}
public String getValue(String key) {
return environment.getProperty(key);
}
- 每个方法中,调用
ConfigurableEnvironment#getProperty(key, defaultValue)
方法,进行转换后返回值。©著作权归作者所有:来自51CTO博客作者mb5ff80520dfa04的原创作品,如需转载,请注明出处,否则将追究法律责任更多相关文章
- MySQL中自增ID起始值修改方法
- Python3版本下创建计算给定日期范围内工作日方法
- 芋道 Spring Boot JPA 入门(二)之基于方法名查询
- DoDAF2.0方法论探究
- http协议请求方法都有哪些?网络安全学习提升
- 【前端词典】8 个提高 JS 性能的方法
- AngularJS 日期时间选择组件(附详细使用方法)
- 5 种方法教你用Python玩转histogram直方图
- IDEA Debug 无法进入断点的解决方法
随机推荐