前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Apollo 源码解析 —— 服务自身配置 ServerConfig

Apollo 源码解析 —— 服务自身配置 ServerConfig

作者头像
芋道源码
发布2020-06-01 15:06:22
1K0
发布2020-06-01 15:06:22
举报
文章被收录于专栏:芋道源码1024芋道源码1024

摘要: 原创出处 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) 方法,进行转换后返回值。?

4.4 PortalConfig

com.ctrip.framework.apollo.portal.component.config.PortalConfig ,实现 RefreshableConfig 抽象类。

4.4.1 getRefreshablePropertySources

@Autowired
private PortalDBPropertySource portalDBPropertySource;

@Override
public List<RefreshablePropertySource> getRefreshablePropertySources() {
    return Collections.singletonList(portalDBPropertySource);
}
  • 返回 PortalDBPropertySource 对象的数组。

4.4.2 获得值

方法比较多,胖友自己查看。如下是一个例子:

// 获得 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;
}

4.5 BizConfig

com.ctrip.framework.apollo.biz.config.BizConfig ,实现 RefreshableConfig 抽象类。

4.5.1 getRefreshablePropertySources

@Autowired
private BizDBPropertySource propertySource;

@Override
protected List<RefreshablePropertySource> getRefreshablePropertySources() {
    return Collections.singletonList(propertySource);
}
  • 返回 BizDBPropertySource 对象的数组。

4.5.2 获得值

方法比较多,胖友自己查看。如下是一个例子:

// 获得 Eureka 服务器地址的数组
public List<String> eurekaServiceUrls() {
    // 获得配置值
    String configuration = getValue("eureka.service.url", "");
    // 分隔成 List
    if (Strings.isNullOrEmpty(configuration)) {
        return Collections.emptyList();
    }
    return splitter.splitToList(configuration);
}

5. ServerConfigController

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

}
  • 对应 Portal 界面如下图:
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-05-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 芋道源码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 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.4.1 getRefreshablePropertySources
                    • 4.4.2 获得值
                  • 4.5 BizConfig
                    • 4.5.1 getRefreshablePropertySources
                    • 4.5.2 获得值
                • 5. ServerConfigController
                相关产品与服务
                云数据库 Redis
                腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档