摘要: 原创出处 http://www.iocoder.cn/Apollo/server-config/ 「芋道源码」欢迎转载,保留摘要,谢谢!
老艿艿:本系列假定胖友已经阅读过 《Apollo 官方 wiki 文档》
Portal、Config Service、Admin Service 等等服务,自身需要配置服务。一种实现是,基于配置文件,简单方便。但是,不方便统一管理和共享。因此,Apollo 基于数据库实现类配置表 ServerConfig 。
老艿艿:如果胖友的系统暂时没有使用配置中心,
本文涉及的类如下图所示:
类图
在 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;
}
在 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;
}
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"
集群。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();
}
#refresh()
抽象方法,刷新配置。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);
}
}
}
"DBConfig"
,属性源使用 ConcurrentMap 。#refresh()
实现方法,从 PortalDB 中,读取所有的 ServerConfig 记录,更新到属性源 source
。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);
}
}
}
#refresh()
实现方法中,按照默认 的 Cluster、数据中心的 Cluster、JVM 启动参数的 Cluster ,逐个匹配 ServerConfig 的 cluster
字段。若匹配,最终会更新到属性源。com.ctrip.framework.apollo.common.config.RefreshableConfig
,可刷新的配置抽象类。
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 数组。#setup()
初始化方法中,将自己添加到 environment
中。#getRefreshablePropertySources()
抽象方法,返回需要注册的 RefreshablePropertySource 数组。代码如下:
protected
abstract List<RefreshablePropertySource> getRefreshablePropertySources();
environment
属性,Spring ConfigurableEnvironment 对象。其 PropertySource 不仅仅包括 propertySources
,还包括 yaml
properties
等 PropertySource 。这就是为什么 ServerConfig 被封装成 PropertySource 的原因。CONFIG_REFRESH_INTERVAL
静态属性,每 60 秒,刷新一次 propertySources
配置。#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: }
#getRefreshablePropertySources()
方法,获得 RefreshablePropertySource 数组。ConfigurableEnvironment#getPropertySources()#addLast(propertySource)
方法,将 propertySources
注册到 environment
中。RefreshablePropertySource#refresh()
方法,刷新 propertySources
的配置。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)
方法,进行转换后返回值。?com.ctrip.framework.apollo.portal.component.config.PortalConfig
,实现 RefreshableConfig 抽象类。
@Autowired
private PortalDBPropertySource portalDBPropertySource;
@Override
public List<RefreshablePropertySource> getRefreshablePropertySources() {
return Collections.singletonList(portalDBPropertySource);
}
方法比较多,胖友自己查看。如下是一个例子:
// 获得 Env 集合
public List<Env> portalSupportedEnvs() {
// 获得配置项
String[] configurations = getArrayProperty("apollo.portal.envs", new String[]{"FAT", "UAT", "PRO"});
// 创建成 List
List<Env> envs = Lists.newLinkedList();
for (String env : configurations) {
envs.add(Env.fromString(env));
}
return envs;
}
com.ctrip.framework.apollo.biz.config.BizConfig
,实现 RefreshableConfig 抽象类。
@Autowired
private BizDBPropertySource propertySource;
@Override
protected List<RefreshablePropertySource> getRefreshablePropertySources() {
return Collections.singletonList(propertySource);
}
方法比较多,胖友自己查看。如下是一个例子:
// 获得 Eureka 服务器地址的数组
public List<String> eurekaServiceUrls() {
// 获得配置值
String configuration = getValue("eureka.service.url", "");
// 分隔成 List
if (Strings.isNullOrEmpty(configuration)) {
return Collections.emptyList();
}
return splitter.splitToList(configuration);
}
在 apollo-portal
项目中,com.ctrip.framework.apollo.portal.controller.ServerConfigController
,提供 ServerConfig 的 API 。代码如下:
@RestController
public class ServerConfigController {
@Autowired
private ServerConfigRepository serverConfigRepository;
@Autowired
private UserInfoHolder userInfoHolder;
@PreAuthorize(value = "@permissionValidator.isSuperAdmin()")
@RequestMapping(value = "/server/config", method = RequestMethod.POST)
public ServerConfig createOrUpdate(@RequestBody ServerConfig serverConfig) {
// 校验 ServerConfig 非空
checkModel(Objects.nonNull(serverConfig));
// 校验 ServerConfig 的 `key` `value` 属性非空
RequestPrecondition.checkArgumentsNotEmpty(serverConfig.getKey(), serverConfig.getValue());
// 获得操作人为当前管理员
String modifiedBy = userInfoHolder.getUser().getUserId();
// 查询当前 DB 里的对应的 ServerConfig 对象
ServerConfig storedConfig = serverConfigRepository.findByKey(serverConfig.getKey());
// 若不存在,则进行新增
if (Objects.isNull(storedConfig)) {//create
serverConfig.setDataChangeCreatedBy(modifiedBy);
serverConfig.setDataChangeLastModifiedBy(modifiedBy);
return serverConfigRepository.save(serverConfig);
// 若存在,则进行更新
} else { //update
BeanUtils.copyEntityProperties(serverConfig, storedConfig); // 复制属性,serverConfig => storedConfig
storedConfig.setDataChangeLastModifiedBy(modifiedBy);
return serverConfigRepository.save(storedConfig);
}
}
}