攜程 Apollo 配置中心傳統 .NET 項目集成實踐

  • 2019 年 10 月 3 日
  • 筆記

官方文檔存在的問題

可能由於 Apollo 配置中心的客戶端源碼一直處於更新中,導致其相關文檔有些跟不上節奏,部分文檔寫的不規範,很容易給做對接的新手朋友造成誤導。

比如,我在參考如下兩個文檔使用傳統 .NET 客戶端做接入的時候就發現了些問題。

問題一:兩個文檔關於標識應用身份的AppId的配置節點不一致。

問題二:第二個文檔關於應用配置發布環境的Environment配置節點的描述出現明顯錯誤。

當然,這些問題隨時都有可能被修復。若您看到文檔內容與本文描述不符,請以官方文檔為準。

傳統 .NET 項目快速接入

快速進入正題。

安裝依賴包

在您項目的基礎設施層,通過 NuGet 包管理器或使用如下命令添加傳統 .NET 項目使用的客戶端:

Install-Package Com.Ctrip.Framework.Apollo.ConfigurationManager -Version 2.0.3

從上面的包名能看出什麼?我這裡選裝的是2.0.3的版本。還有,這應該是一個 Javaer 起的名字。

配置應用標識 & 服務地址

在您的啟動項目中,打開App.configWeb.config配置文件,在<appSettings>節點中增加如下節點:

<!-- Change to the actual app id -->  <add key="Apollo.AppID" value="R01001" />  <add key="Apollo.MetaServer" value="http://localhost:8080" />

若您部署了多套 Config Service,支援多環境,請參考如下配置:

<!-- Change to the actual app id -->  <add key="Apollo.AppID" value="R01001" />    <!-- Should change the apollo config service url for each environment -->  <add key="Apollo.Env" value="DEV" />  <add key="Apollo.DEV.Meta" value="http://localhost:8080"/>  <add key="Apollo.FAT.Meta" value="http://localhost:8081"/>  <add key="Apollo.UAT.Meta" value="http://localhost:8082"/>  <add key="Apollo.PRO.Meta" value="http://localhost:8083"/>

配置完成後,就可以準備在我們項目中使用 Apollo 客戶端了。

二次封裝程式碼

我們習慣在項目中使用第三方庫的時候封裝一層,這種封裝是淺層的,一般都是在項目的基礎設施層來做,這樣其他層使用就不需要再次引入依賴包。

不說了,直接上程式碼吧。

程式碼結構大致如下:

├─MyCompany.MyProject.Infrastructure         # 項目基礎設施層  │  │  │  └─Configuration  │          ApolloConfiguration.cs            # Apollo 分散式配置項讀取實現  │          ConfigurationChangeEventArgs.cs   # 配置更改回調事件參數  │          IConfiguration.cs                 # 配置抽象介面,可基於此介面實現本地配置讀取

IConfiguration

using System;  using System.Configuration;    namespace MyCompany.MyProject.Infrastructure  {      /// <summary>      /// 配置抽象介面。      /// </summary>      public interface IConfiguration      {          /// <summary>          /// 配置更改回調事件。          /// </summary>          event EventHandler<ConfigurationChangeEventArgs> ConfigChanged;            /// <summary>          /// 獲取配置項。          /// </summary>          /// <param name="key">鍵</param>          /// <param name="namespaces">命名空間集合</param>          /// <returns></returns>          string GetValue(string key, params string[] namespaces);            /// <summary>          /// 獲取配置項。          /// </summary>          /// <typeparam name="TValue">值類型</typeparam>          /// <param name="key">鍵</param>          /// <param name="namespaces">命名空間集合</param>          /// <returns></returns>          TValue GetValue<TValue>(string key, params string[] namespaces);            /// <summary>          /// 獲取配置項,如果值為 <see cref="null"/> 則取參數 <see cref="defaultValue"/> 值。          /// </summary>          /// <param name="key">鍵</param>          /// <param name="defaultValue">默認值</param>          /// <param name="namespaces">命名空間集合</param>          /// <returns></returns>          string GetDefaultValue(string key, string defaultValue, params string[] namespaces);            /// <summary>          /// 獲取配置項,如果值為 <see cref="null"/> 則取參數 <see cref="defaultValue"/> 值。          /// </summary>          /// <typeparam name="TValue">值類型</typeparam>          /// <param name="key">鍵</param>          /// <param name="defaultValue">默認值</param>          /// <param name="namespaces">命名空間集合</param>          /// <returns></returns>          TValue GetDefaultValue<TValue>(string key, TValue defaultValue, params string[] namespaces);      }  }

ConfigurationChangeEventArgs

using Com.Ctrip.Framework.Apollo.Model;  using System.Collections.Generic;    namespace MyCompany.MyProject.Infrastructure  {      public class ConfigurationChangeEventArgs      {          public IEnumerable<string> ChangedKeys => Changes.Keys;          public bool IsChanged(string key) => Changes.ContainsKey(key);          public string Namespace { get; }          public IReadOnlyDictionary<string, ConfigChange> Changes { get; }          public ConfigurationChangeEventArgs(string namespaceName, IReadOnlyDictionary<string, ConfigChange> changes)          {              Namespace = namespaceName;              Changes = changes;          }          public ConfigChange GetChange(string key)          {              Changes.TryGetValue(key, out var change);              return change;          }      }  }

ApolloConfiguration

using System;  using System.Configuration;  using System.Globalization;  using Com.Ctrip.Framework.Apollo;  using Com.Ctrip.Framework.Apollo.Model;    namespace MyCompany.MyProject.Infrastructure  {      public class ApolloConfiguration : IConfiguration      {          private readonly string _defaultValue = null;            public event EventHandler<ConfigurationChangeEventArgs> ConfigChanged;            private IConfig GetConfig(params string[] namespaces)          {              var config = namespaces == null || namespaces.Length == 0 ?                  ApolloConfigurationManager.GetAppConfig().GetAwaiter().GetResult() :                  ApolloConfigurationManager.GetConfig(namespaces).GetAwaiter().GetResult();                config.ConfigChanged += (object sender, ConfigChangeEventArgs args) =>              {                  ConfigChanged(sender, new ConfigurationChangeEventArgs(args.Namespace, args.Changes));              };                return config;          }            public string GetValue(string key, params string[] namespaces)          {              key = key ?? throw new ArgumentNullException(nameof(key));              var config = GetConfig(namespaces);              return config.GetProperty(key, _defaultValue);          }            public TValue GetValue<TValue>(string key, params string[] namespaces)          {              var value = GetValue(key, namespaces);              return value == null ?                  default(TValue) :                  (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture);          }            public string GetDefaultValue(string key, string defaultValue, params string[] namespaces)          {              key = key ?? throw new ArgumentNullException(nameof(key));              var config = GetConfig(namespaces);              return config.GetProperty(key, defaultValue);          }            public TValue GetDefaultValue<TValue>(string key, TValue defaultValue, params string[] namespaces)          {              var value = GetDefaultValue(key, defaultValue, namespaces);              return value == null ?                  default(TValue) :                  (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture);          }      }  }

正確使用姿勢

在使用之前需要先把ApolloConfiguration註冊到應用容器中,請參考如下程式碼:

public class DependencyRegistrar : IDependencyRegistrar  {      public void Register(ContainerBuilder builder, ITypeFinder typeFinder)      {          // 我們項目使用的 DI 框架是 Autofac,註冊這個地方按需修改吧,注意將實例註冊成單例。          builder.RegisterType<ApolloConfiguration>()              .As<IConfiguration>()              .Named<IConfiguration>("configuration")              .SingleInstance();            ...      }        public int Order      {          get { return 1; }      }  }

接下來就可以在項目中使用了,請參考如下程式碼:

public class UserController : BaseController  {      private readonly IConfiguration _configuration;        public UserController(IConfiguration configuration)      {          _configuration = configuration;      }        public ActionResult Add(AddUserInput model)      {          if (ModelState.IsValid)          {              // 從 Apollo 分散式配置中心 項目 R01001 默認命名空間`application`下 讀取配置項。              model.Password = _configuration.GetValue("DefaultUserPassword");              ...          }          ...      }  }