某些時候我們需要為HttpClient動態配置一些東西, 例如證書等, 參考博問 如何使用IHttpClientFactory動態添加cer證書. 例如服務是一個回調服務, 而被回調方采用了自定義的https(即自定義證書).
上述是一些前情概要, 那么接下來我們就來實現這個需求.
秒想到一個方法, 我們可以直接new HttpClient(), 在每一次要使用的時候都直接來一個, 簡單粗暴.
秒想到第二個方法, 又或者用一個Dictionary<string,HttpClient>根據名字緩存client對象.
但是前者性能是個問題,而且涉及到端口的占用釋放問題, 在調用量稍大的情況下得涼涼, 后者則是有已知的問題HttpClient對象沒法感知dns的變更.
其他一些更不靠譜的方法還有: 使用代碼配置方式(services.AddHttpClient("callback provider side").ConfigurePrimaryHttpMessageHandler())配置所有證書, 還有把所有證書都安裝的本機上並設置為信任證書.
那么能除了上面這些不靠譜的方式(或者說有致命缺陷的方式), 還有靠譜的么, 那當然是有的, 例如運行時的動態配置實現方案.
所以, 接下來, 我來推薦 2 種方式式,就是我們的IHttpMessageHandlerBuilderFilter和IPostConfigureOptions.
官方有什么推薦么?
針對如何為HttpClient對象添加證書, 官方文檔的實現是:使用證書和來自 IHttpClientFactory 的命名 HttpClient 實現 HttpClient 和 使用證書和 HttpClientHandler 實現 HttpClient, 但是在這里顯然沒法解決我們的運行時配置的需求, 但是它給出了一條線索, 那就是命名配置. 它可以為我們的每一個不同的provider提供自定義配置. 只要我們能為每一個不同的provider能提供運行時配置即可, 接下來就是源碼閱讀時間了:
下文中的所有代碼都來自netcore 3.1, 並且僅copy關鍵代碼, 完整代碼可以前往github查看.
IHttpClientFactory.CreateClient是如何將HttpClient創建出來的?
- 每次
CreateClient出來的都是一個新的HttpClient實例 - 在
CreateHandler中的_activeHandlers將為我們緩存我們的handler, 默認是2分鍾(定義在HttpClientFactoryOptions.HandlerLifetime)- 這里有一個知識點就是如果我的請求剛好在過期時間前一點點獲取到這個緩存的對象,就是有可能我當前的請求還在進行中, 但是2分鍾過去后這個handler就要被回收的. 那官方是如何替我們解決這個可能的bug的呢, 請查看文章Cleaning up expired handlers, 我就不贅述了, 關鍵點在於用了一個
WeakReference
- 這里有一個知識點就是如果我的請求剛好在過期時間前一點點獲取到這個緩存的對象,就是有可能我當前的請求還在進行中, 但是2分鍾過去后這個handler就要被回收的. 那官方是如何替我們解決這個可能的bug的呢, 請查看文章Cleaning up expired handlers, 我就不贅述了, 關鍵點在於用了一個
CreateHandlerEntry方法則是真正的創建以及配置我們的handlers的地方.- 從
IConfiguration獲得一個HttpClientFactoryOptions對象 - 應用
IHttpMessageHandlerBuilderFilter - 應用
HttpMessageHandlerBuilderActions
- 從
//Microsoft.Extensions.Http.DefaultHttpClientFactory
public HttpClient CreateClient(string name)
{
HttpClient httpClient = new HttpClient(this.CreateHandler(name), disposeHandler: false);
return httpClient;
}
public HttpMessageHandler CreateHandler(string name)
{
ActiveHandlerTrackingEntry value = this._activeHandlers.GetOrAdd(name, this._entryFactory).Value;
//_entryFactory可以直接理解為是CreateHandlerEntry方法.它真實的類型是Lazy<>(CreateHandlerEntry,LazyThreadSafetyMode.ExecutionAndPublication)的, 也就是並發安全的調用CreateHandlerEntry.
return value.Handler;
}
internal ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
HttpClientFactoryOptions options = this._optionsMonitor.Get(name);
HttpMessageHandlerBuilder requiredService = provider.GetRequiredService<HttpMessageHandlerBuilder>();
requiredService.Name = name;
Action<HttpMessageHandlerBuilder> action = Configure; // 擴展點二 HttpClientFactoryOptions.HttpMessageHandlerBuilderActions
for (int num = this._filters.Length - 1; num >= 0; num--)
{
action = this._filters[num].Configure(action); //擴展點一 _filters(構造函數傳入的IEnumerable<IHttpMessageHandlerBuilderFilter> filters).
}
action(requiredService);
LifetimeTrackingHttpMessageHandler handler = new LifetimeTrackingHttpMessageHandler(requiredService.Build());
return new ActiveHandlerTrackingEntry(name, handler, serviceScope, options.HandlerLifetime);
void Configure(HttpMessageHandlerBuilder b)
{
for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
{
options.HttpMessageHandlerBuilderActions[i](b);
}
}
}
關鍵點代碼就是上面代碼中標記出來的擴展點一 和 擴展點二.
- 擴展點一: 需要注入適當的IHttpMessageHandlerBuilderFilter對象,就可以改寫
requiredService對象, 也就可以實現我們要的運行時動態配置了. - 擴展點二: 需要實現自定義的IConfiguration配置, 只要
this._optionsMonitor.Get(name)拿到的對象的HttpMessageHandlerBuilderActions屬性包含我們相應的改寫代碼即可.
擴展點一的實現
為HttpClient的handler增加一個配置的filter, 針對符合的handlerBuilder增加一些自己的改寫邏輯.
我們在用HttpClient對象的時候產生的日志("Sending HTTP request......","Received HTTP response headers after......")就是由這個Filter特性注入的. 官方參考代碼:LoggingHttpMessageHandlerBuilderFilter
個人見解: 覺得在這個擴展點加這個業務不是特別的符合應用場景, 所以我建議在擴展點二做這個事情.
class MyHttpClientHandlerFilter : IHttpMessageHandlerBuilderFilter
{
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
void Configure(HttpMessageHandlerBuilder builder)
{
next(builder); //一開始就調用next, 這樣我們的整個HandlerBuilder的執行順序就是依次call _filters, 最后call options.HttpMessageHandlerBuilderActions(擴展點二).
if (builder.Name.StartsWith("CallbackProviderSide-")) //我們可以為這類業務統一加一個前綴做區別, 這樣就不會影響其他的HttpClient對象了.
{
//builder.PrimaryHandler= your custom handler. 參考官方文檔的實現.
}
}
return Configure;
}
}
//然后在DI容器中注入我們的filter.
ServiceCollection.AddSingleton<IHttpMessageHandlerBuilderFilter,MyHttpClientHandlerFilter>();
擴展點二的實現
class MyHttpClientCustomConfigure : IPostConfigureOptions<HttpClientFactoryOptions>
{
public void PostConfigure(string name, HttpClientFactoryOptions options)
{
if (name.StartsWith("CallbackProviderSide-")) //我們可以為這類業務統一加一個前綴做區別, 這樣就不會影響其他的HttpClient對象了.
{
options.HttpMessageHandlerBuilderActions.Add(p =>
{
//p.PrimaryHandler= your custom handler. 參考官方文檔的實現.
});
}
}
}
//然后在DI容器中注入我們的這個配置擴展類.
ServiceCollection.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.Extensions.Http.HttpClientFactoryOptions>, MyHttpClientCustomConfigure>();
為什么這里注入的類型是Microsoft.Extensions.Options.IPostConfigureOptions<Microsoft.Extensions.Http.HttpClientFactoryOptions>, 是因為OptionsFactory它的構造函數需要的就是這個. 至於有關Configuration系統的擴展和源代碼在這里就不在這里展開了.
使用
至於用它就簡單了
var factory = ServiceProvider.GetService<IHttpClientFactory>();
var httpClientForBaidu = factory.CreateClient("CallbackProviderSide-baidu");
var httpClientForCnblogs = factory.CreateClient("CallbackProviderSide-Cnblogs");
總結一下
這樣子, 我們的這個運行時動態配置HttpClient就算完成了, 我也輕輕松松又水了一篇文章.
另外,有關IHttpClientFactory背后的故事可以查看文章Exploring the code behind IHttpClientFactory in depth, 很完整的流程圖在配上代碼, 把它講解的清清楚楚.
