1. 概述
本文接 一文,分享 Config Service 配置读取的接口的实现。在上文,我们看到通知变化接口,仅返回通知相关的信息,而不包括配置相关的信息。所以 Config Service 需要提供配置读取的接口。
? 为什么不在通知变化的同时,返回最新的配置信息呢?(哈哈哈)
OK,让我们开始看看具体的代码实现。
2. ConfigController
com.ctrip.framework.apollo.configservice.controller.ConfigController
,配置 Controller ,仅提供 configs/{appId}/{clusterName}/{namespace:.+}
接口,提供配置读取的功能。
2.1 构造方法
private static final Splitter X_FORWARDED_FOR_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();private static final Type configurationTypeReference = new TypeToken |
2.2 queryConfig
1: @RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET) 2: public ApolloConfig queryConfig(@PathVariable String appId, @PathVariable String clusterName, 3: @PathVariable String namespace, 4: @RequestParam(value = "dataCenter", required = false) String dataCenter, 5: @RequestParam(value = "releaseKey", defaultValue = "-1") String clientSideReleaseKey, 6: @RequestParam(value = "ip", required = false) String clientIp, 7: @RequestParam(value = "messages", required = false) String messagesAsString, 8: HttpServletRequest request, HttpServletResponse response) throws IOException { 9: String originalNamespace = namespace;10: // 若 Namespace 名以 .properties 结尾,移除该结尾,并设置到 ApolloConfigNotification 中。例如 application.properties => application 。11: // strip out .properties suffix12: namespace = namespaceUtil.filterNamespaceName(namespace);13: // 获得归一化的 Namespace 名字。因为,客户端 Namespace 会填写错大小写。14: //fix the character case issue, such as FX.apollo <-> fx.apollo15: namespace = namespaceUtil.normalizeNamespace(appId, namespace);16: 17: // 若 clientIp 未提交,从 Request 中获取。18: if (Strings.isNullOrEmpty(clientIp)) {19: clientIp = tryToGetClientIp(request);20: }21: 22: // 解析 messagesAsString 参数,创建 ApolloNotificationMessages 对象。23: ApolloNotificationMessages clientMessages = transformMessages(messagesAsString);24: 25: // 创建 Release 数组26: List |
- GET
/configs/{appId}/{clusterName}/{namespace:.+}
接口,指定 Namespace 的配置读取。在 中,有该接口的接口定义说明。 clientSideReleaseKey
请求参数,客户端侧的 Release Key ,用于和获得的 Release 的releaseKey
对比,判断是否有配置更新。clientIp
请求参数,客户端 IP ,用于灰度发布的功能。? 本文会跳过和灰度发布相关的内容,后续文章单独分享。-
messagesAsString
请求参数,客户端当前请求的 Namespace 的通知消息明细,在【第 23 行】中,调用#transformMessages(messagesAsString)
方法,解析messagesAsString
参数,创建 ApolloNotificationMessages 对象。在 中,我们已经看到通知变更接口返回的就包括 ApolloNotificationMessages 对象。#transformMessages(messagesAsString)
方法,代码如下:ApolloNotificationMessages transformMessages(String messagesAsString) { ApolloNotificationMessages notificationMessages = null; if (!Strings.isNullOrEmpty(messagesAsString)) { try { notificationMessages = gson.fromJson(messagesAsString, ApolloNotificationMessages.class); } catch (Throwable ex) { Tracer.logError(ex); } } return notificationMessages;}
-
第 12 行:调用
NamespaceUtil#filterNamespaceName(namespaceName)
方法,若 Namespace 名以".properties"
结尾,移除该结尾。 - 第 15 行:调用
NamespaceUtil#normalizeNamespace(appId, originalNamespace)
方法,获得归一化的 Namespace 名字。因为,客户端 Namespace 会填写错大小写。 - 第 17 至 20 行:若客户端未提交
clientIp
,调用#tryToGetClientIp(HttpServletRequest)
方法,获取 IP 。详细解析,见 方法。 - ========== 分割线 ==========
- 第 26 行:创建 Release 数组。
- 第 27 至 39 行:获得 Namespace 对应的最新的 Release 对象。
- 第 31 行:调用
ConfigService#loadConfig(appId, clientIp, appId, clusterName, namespace, dataCenter, clientMessages)
方法,获得 Release 对象。详细解析,见 方法。 - 第 34 行:添加到 Release 书中。
- 第 37 行:获得 Release 对应的 Cluster 名字。因为,在
ConfigService#loadConfig(appId, clientIp, appId, clusterName, namespace, dataCenter, clientMessages)
方法中,会根据clusterName
和dataCenter
分别查询 Release 直到找到一个,所以需要根据结果的 Release 获取真正的 Cluster 名。
- 第 31 行:调用
-
第 40 至 49 行:若 Namespace 为关联类型,则获取关联的 Namespace 的最新的 Release 对象。
-
第 42 行:调用
#namespaceBelongsToAppId(appId, namespace)
方法,判断 Namespace 非当前 App 下的,这是关联类型的前提。代码如下:private boolean namespaceBelongsToAppId(String appId, String namespaceName) { // Namespace 非 'application' ,因为每个 App 都有 // Every app has an 'application' namespace if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) { return true; } // App 编号非空 // if no appId is present, then no other namespace belongs to it if (ConfigConsts.NO_APPID_PLACEHOLDER.equalsIgnoreCase(appId)) { return false; } // 非当前 App 下的 Namespace AppNamespace appNamespace = appNamespaceService.findByAppIdAndNamespace(appId, namespaceName); return appNamespace != null;}
- x
-
第 44 行:调用
#findPublicConfig(...)
方法,获得公用类型的 Namespace 的 Release 对象。代码如下:private Release findPublicConfig(String clientAppId, String clientIp, String clusterName, String namespace, String dataCenter, ApolloNotificationMessages clientMessages) { // 获得公用类型的 AppNamespace 对象 AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace); // 判断非当前 App 下的,那么就是关联类型。 // check whether the namespace's appId equals to current one if (Objects.isNull(appNamespace) || Objects.equals(clientAppId, appNamespace.getAppId())) { return null; } String publicConfigAppId = appNamespace.getAppId(); // 获得 Namespace 最新的 Release 对象 return configService.loadConfig(clientAppId, clientIp, publicConfigAppId, clusterName, namespace, dataCenter, clientMessages);}
- 在其内部,也是调用
ConfigService#loadConfig(appId, clientIp, appId, clusterName, namespace, dataCenter, clientMessages)
方法,获得 Namespace 最新的 Release 对象。
- 在其内部,也是调用
- 第 45 至 48 行:添加到 Release 数组中。
-
- 第 50 至 56 行:若获得不到 Release ,返回状态码为 404 的响应。
- ========== 分割线 ==========
- 第 59 行:调用
#auditReleases(...)
方法,记录 InstanceConfig 。详细解析,见 。 - ========== 分割线 ==========
- 第 62 行:计算 Config Service 的合并 ReleaseKey 。当有多个 Release 时,使用
"+"
作为字符串的分隔。 - 第 64 至 69 行:对比 Client 的合并 Release Key 。若相等,说明配置没有改变,返回状态码为 302 的响应。
- ========== 分割线 ==========
- 第 72 行:创建 ApolloConfig 对象。详细解析,见 方法。
- 第 74 行:调用
#mergeReleaseConfigurations(List<Release)
方法,合并多个 Release 的配置集合,并将结果设置到 ApolloConfig 中。详细解析,见 方法。 - 第 77 行:【TODO 6001】Tracer 日志
- 第 78 行:返回 ApolloConfig 对象。
2.3 tryToGetClientIp
#tryToGetClientIp(HttpServletRequest)
方法,从请求中获取 IP 。代码如下:
private String tryToGetClientIp(HttpServletRequest request) { String forwardedFor = request.getHeader("X-FORWARDED-FOR"); if (!Strings.isNullOrEmpty(forwardedFor)) { return X_FORWARDED_FOR_SPLITTER.splitToList(forwardedFor).get(0); } return request.getRemoteAddr();} |
- 关于
"X-FORWARDED-FOR"
Header ,详细解析见 。
2.4 mergeReleaseConfigurations
#mergeReleaseConfigurations(List<Release)
方法,合并多个 Release 的配置集合。代码如下:
Map |
- 为什么要反转数组?因为关联类型的 Release 后添加到 Release 数组中。但是,App 下 的 Release 的优先级更高,所以进行反转。
3. ConfigService
com.ctrip.framework.apollo.configservice.service.config.ConfigService
,实现 ReleaseMessageListener 接口,配置 Service 接口。代码如下:
public interface ConfigService extends ReleaseMessageListener { Release loadConfig(String clientAppId, String clientIp, String configAppId, String configClusterName, String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages);} |
子类如下图所示:
最终有两个子类,差异点在于是否使用缓存,通过 ServerConfig "config-service.cache.enabled"
配置,默认关闭。开启后能提高性能,但是会增大内存消耗!
在 ConfigServiceAutoConfiguration 中,初始化使用的 ConfigService 实现类,代码如下:
@Autowiredprivate BizConfig bizConfig;@Beanpublic ConfigService configService() { // 开启缓存,使用 ConfigServiceWithCache if (bizConfig.isConfigServiceCacheEnabled()) { return new ConfigServiceWithCache(); } // 不开启缓存,使用 DefaultConfigService return new DefaultConfigService();} |
3.1 AbstractConfigService
com.ctrip.framework.apollo.configservice.service.config.AbstractConfigService
,实现 ConfigService 接口,配置 Service 抽象类,实现公用的获取配置的逻辑,并暴露抽象方法,让子类实现。抽象方法如下:
protected abstract Release findActiveOne(long id, ApolloNotificationMessages clientMessages);protected abstract Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespaceName, ApolloNotificationMessages clientMessages); |
3.1.1 loadConfig
#loadConfig(...)
实现方法,代码如下:
1: @Override 2: public Release loadConfig(String clientAppId, String clientIp, String configAppId, String configClusterName, 3: String configNamespace, String dataCenter, ApolloNotificationMessages clientMessages) { 4: // 优先,获得指定 Cluster 的 Release 。若存在,直接返回。 5: // load from specified cluster fist 6: if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, configClusterName)) { 7: Release clusterRelease = findRelease(clientAppId, clientIp, configAppId, configClusterName, configNamespace, 8: clientMessages); 9: if (!Objects.isNull(clusterRelease)) {10: return clusterRelease;11: }12: }13: 14: // 其次,获得所属 IDC 的 Cluster 的 Release 。若存在,直接返回15: // try to load via data center16: if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, configClusterName)) {17: Release dataCenterRelease = findRelease(clientAppId, clientIp, configAppId, dataCenter, configNamespace, clientMessages);18: if (!Objects.isNull(dataCenterRelease)) {19: return dataCenterRelease;20: }21: }22: 23: // 最后,获得默认 Cluster 的 Release 。24: // fallback to default release25: return findRelease(clientAppId, clientIp, configAppId, ConfigConsts.CLUSTER_NAME_DEFAULT, configNamespace, clientMessages);26: } |
- 第 4 至 12 行:优先,获得指定 Cluster 的 Release 。若存在,直接返回。
- 第 14 至 21 行:其次,获得所属 IDC 的 Cluster 的 Release 。若存在,直接返回。
- 第 25 行:最后,获得默认的 Cluster 的 Release 。
- 每一次获取,都调用了
#findRelease(...)
方法,获取对应的 Release 对象。详细解析,见 方法。 - 关于多 Cluster 的读取顺序,可参见 。这块的代码,就是实现该顺序,如下图:
3.1.2 findRelease
1: 14: private Release findRelease(String clientAppId, String clientIp, String configAppId, String configClusterName,15: String configNamespace, ApolloNotificationMessages clientMessages) {16: // 读取灰度发布编号17: Long grayReleaseId = grayReleaseRulesHolder.findReleaseIdFromGrayReleaseRule(clientAppId, clientIp, configAppId, configClusterName, configNamespace);18: // 读取灰度 Release 对象19: Release release = null;20: if (grayReleaseId != null) {21: release = findActiveOne(grayReleaseId, clientMessages);22: }23: // 非灰度,获得最新的,并且有效的 Release 对象24: if (release == null) {25: release = findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages);26: }27: return release;28: } |
- 第 17 行:调用
GrayReleaseRulesHolder#findReleaseIdFromGrayReleaseRule(...)
方法,读取灰度发布编号,即GrayReleaseRule.releaseId
属性。详细解析,在 中。 - 第 18 至 22 行:调用
#findActiveOne(grayReleaseId, clientMessages)
方法,读取灰度 Release 对象。 - 第 23 至 26 行:若非灰度,调用
#findLatestActiveRelease(configAppId, configClusterName, configNamespace, clientMessages)
方法,获得最新的,并且有效的 Release 对象。
3.3 DefaultConfigService
com.ctrip.framework.apollo.configservice.service.config.DefaultConfigService
,实现 AbstractConfigService 抽象类,配置 Service 默认实现类,直接查询数据库,而不使用缓存。代码如下:
public class DefaultConfigService extends AbstractConfigService { @Autowired private ReleaseService releaseService; @Override protected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) { return releaseService.findActiveOne(id); } @Override protected Release findLatestActiveRelease(String configAppId, String configClusterName, String configNamespace, ApolloNotificationMessages clientMessages) { return releaseService.findLatestActiveRelease(configAppId, configClusterName, configNamespace); } @Override public void handleMessage(ReleaseMessage message, String channel) { // since there is no cache, so do nothing }} |
- ReleaseService ,在 中,有详细解析。
3.4 ConfigServiceWithCache
com.ctrip.framework.apollo.configservice.service.config.ConfigServiceWithCache
,实现 AbstractConfigService 抽象类,基于 Guava Cache 的配置 Service 实现类。
3.4.1 构造方法
private static final Logger logger = LoggerFactory.getLogger(ConfigServiceWithCache.class);private static final long DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES = 60; //1 hour// TRACER 日志内存的枚举private static final String TRACER_EVENT_CACHE_INVALIDATE = "ConfigCache.Invalidate";private static final String TRACER_EVENT_CACHE_LOAD = "ConfigCache.LoadFromDB";private static final String TRACER_EVENT_CACHE_LOAD_ID = "ConfigCache.LoadFromDBById";private static final String TRACER_EVENT_CACHE_GET = "ConfigCache.Get";private static final String TRACER_EVENT_CACHE_GET_ID = "ConfigCache.GetById";private static final Splitter STRING_SPLITTER = Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();@Autowiredprivate ReleaseService releaseService;@Autowiredprivate ReleaseMessageService releaseMessageService;private LoadingCache |
3.4.2 ConfigCacheEntry
ConfigCacheEntry ,ConfigServiceWithCache 的内部私有静态类,配置缓存 Entry 。代码如下:
private static class ConfigCacheEntry { private final long notificationId; private final Release release; public ConfigCacheEntry(long notificationId, Release release) { this.notificationId = notificationId; this.release = release; } public long getNotificationId() { return notificationId; } public Release getRelease() { return release; }} |
3.4.3 初始化
#initialize()
方法,通过 Spring 调用,初始化缓存对象。代码如下:
1: @PostConstruct 2: void initialize() { 3: // 初始化 configCache 4: configCache = CacheBuilder.newBuilder() 5: .expireAfterAccess(DEFAULT_EXPIRED_AFTER_ACCESS_IN_MINUTES, TimeUnit.MINUTES) // 访问过期 6: .build(new CacheLoader |
- 第 4 至 41 行:初始化
configCache
。- 第 9 至 14 行:
key
格式不正确,返回nullConfigCacheEntry
。 - 第 19 行:调用
releaseMessageService.findLatestReleaseMessageForMessages(List<String>)
方法,获得最新的ReleaseMessage 对象。这一步是 DefaultConfigService 没有的操作,用于读取缓存的时候,判断缓存是否过期,下文详细解析。 - 第 21 行:调用
ReleaseService.findLatestActiveRelease(appId, clusterName, namespaceName)
方法,获得最新的,且有效的 Release 对象。 - 第 25 行:获得通知编号。
- 第 26 至 29 行:若
latestReleaseMessage
和latestRelease
都为空,返回nullConfigCacheEntry
。 - 第 31 行:创建 ConfigCacheEntry 对象,并返回。
- 第 9 至 14 行:
- 第 42 至 66 行:初始化
configIdCache
。- 第 52 行:调用
ReleaseService#findActiveOne(key)
方法,获得 Release 对象。 - 第 56 行:调用
Optional.ofNullable(Object)
方法,使用 Optional 包装 Release 对象,并返回。
- 第 52 行:调用
3.4.4 handleMessage
1: @Override 2: public void handleMessage(ReleaseMessage message, String channel) { 3: logger.info("message received - channel: {}, message: {}", channel, message); 4: // 仅处理 APOLLO_RELEASE_TOPIC 5: if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(message.getMessage())) { 6: return; 7: } 8: try { 9: // 清空对应的缓存10: invalidate(message.getMessage());11: // 预热缓存,读取 ConfigCacheEntry 对象,重新从 DB 中加载。12: // warm up the cache13: configCache.getUnchecked(message.getMessage());14: } catch (Throwable ex) {15: //ignore16: }17: } |
- 第 4 至 7 行:仅处理 APOLLO_RELEASE_TOPIC 。
-
第 10 行:调用
#invalidate(message)
方法,清空对应的缓存。代码如下:private void invalidate(String key) { // 清空对应的缓存 configCache.invalidate(key); // 【TODO 6001】Tracer 日志 Tracer.logEvent(TRACER_EVENT_CACHE_INVALIDATE, key);}
-
第 13 行:调用
LoadingCache#getUnchecked(key)
方法,预热缓存,读取 ConfigCacheEntry 对象,重新从 DB 中加载。
3.4.5 findLatestActiveRelease
1: @Override 2: protected Release findLatestActiveRelease(String appId, String clusterName, String namespaceName, ApolloNotificationMessages clientMessages) { 3: // 根据 appId + clusterName + namespaceName ,获得 ReleaseMessage 的 `message` 。 4: String key = ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName); 5: // 【TODO 6001】Tracer 日志 6: Tracer.logEvent(TRACER_EVENT_CACHE_GET, key); 7: // 从缓存 configCache 中,读取 ConfigCacheEntry 对象 8: ConfigCacheEntry cacheEntry = configCache.getUnchecked(key); 9: // 若客户端的通知编号更大,说明缓存已经过期。10: // cache is out-dated11: if (clientMessages != null && clientMessages.has(key) && clientMessages.get(key) > cacheEntry.getNotificationId()) {12: // 清空对应的缓存13: // invalidate the cache and try to load from db again14: invalidate(key);15: // 读取 ConfigCacheEntry 对象,重新从 DB 中加载。16: cacheEntry = configCache.getUnchecked(key);17: }18: // 返回 Release 对象19: return cacheEntry.getRelease();20: } |
- 第 4 行:调用
ReleaseMessageKeyGenerator#generate(appId, clusterName, namespaceName)
方法,根据appId
+clusterName
+namespaceName
,获得 ReleaseMessage 的message
。 - 第 8 行:调用
LoadingCache#getUnchecked(key)
方法,从缓存configCache
中,读取 ConfigCacheEntry 对象。 - 第 9 至 17 行:若客户端的通知编号更大,说明缓存已经过期。因为
#handleMessage(ReleaseMessage message, String channel)
方法,是通过定时扫描 ReleaseMessage 的机制实现,那么延迟是不可避免会存在的。所以通过此处比较的方式,实现缓存的过期的检查。- 第 14 行:调用
#invalidate(message)
方法,清空对应的缓存。 - 第 16 行:调用
LoadingCache#getUnchecked(key)
方法,读取 ConfigCacheEntry 对象,重新从 DB 中加载。- 第 19 行:返回 Release 对象。
- 第 14 行:调用
3.4.6 findActiveOne
@Overrideprotected Release findActiveOne(long id, ApolloNotificationMessages clientMessages) { // 【TODO 6001】Tracer 日志 Tracer.logEvent(TRACER_EVENT_CACHE_GET_ID, String.valueOf(id)); // 从缓存 configIdCache 中,读取 Release 对象 return configIdCache.getUnchecked(id).orElse(null);} |
4. ApolloConfig
com.ctrip.framework.apollo.core.dto.ApolloConfig
,Apollo 配置 DTO 。代码如下:
public class ApolloConfig { private String appId; private String cluster; private String namespaceName; private Map |
- 该类在
apollo-core
项目中,被apollo-configservice
和apollo-client
共同引用。因此,Apollo 的客户端,也使用 ApolloConfig 。 - 工作一到五年的java 开发工程师朋友可以加入我们Java架构交流:760940986 领取获取往期Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术