寫在前面
上一篇文章討論了通過在ConfigureServices中呼叫services.AddHttpClient()方法,並基於此進一步探討了DefaultHttpClientFactory是如何建立HttpClient例項和HttpMessageHandler例項的,並瞭解了DefaultHttpClientFactory內部維護者一個定時器和兩個HttpMessageHandler物件集合,以定期清理無效的 HttpMessageHandler物件,詳細的內容可以點選連結跳轉,接下來我會接著前一篇文章繼續展開相關討論。
詳細介紹
HttpMessageHandlerBuilder
該類是一個抽象類,起到生成器的作用,可用於用於配置HttpMessageHandler例項。HttpMessageHandlerBuilder會在ServiceCollection中被註冊為Transient服務。呼叫方要為每個要建立的HttpMessageHandler例項檢索一個新例項。實現者應該確保每個例項都只使用一次。
HttpMessageHandlerBuilder裡面有三個比較重要的屬性:
1: /// <summary>
2: /// 主HttpMessageHandler例項
3: /// </summary>
4: public abstract HttpMessageHandler PrimaryHandler { get; set; }
5:
6: /// <summary>
7: /// 這個是一個附加例項,用於配置HttpClient管道
8: /// </summary>
9: public abstract IList<DelegatingHandler> AdditionalHandlers { get; }
10:
11: /// <summary>
12: /// 可用於從依賴項注入容器解析服務的IServiceProvider
13: /// </summary>
14: public virtual IServiceProvider Services { get; }
這三個屬性意味著每個HttpMessageHandlerBuilder都需要維護自身的HttpMessageHandler例項和管道。
其內部還有一個抽象方法:
1: public abstract HttpMessageHandler Build();
當然,內部最核心的方法就是管道的建立過程了,需要傳入主派生類自身的HttpMessageHandler和管道列表物件。它會將primaryHandler例項付給管道列表的第一個Item的InnerHandler,其他物件會依此後移,這也為我們自定義HttpMessageHandler(各種中介軟體)提供了無限可能。
相關實現如下:
1: var next = primaryHandler;
2: for (var i = additionalHandlersList.Count - 1; i >= 0; i--)
3: {
4: var handler = additionalHandlersList[i];
5: if (handler == null)
6: {
7: var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers));
8: throw new InvalidOperationException(message);
9: }
10:
11: if (handler.InnerHandler != null)
12: {
13: var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid(
14: nameof(DelegatingHandler.InnerHandler),
15: nameof(DelegatingHandler),
16: nameof(HttpMessageHandlerBuilder),
17: Environment.NewLine,
18: handler);
19: throw new InvalidOperationException(message);
20: }
21:
22: handler.InnerHandler = next;
23: next = handler;
24: }
接下來我們看一下HttpMessageHandlerBuilder一個派生類DefaultHttpMessageHandlerBuilder,其建構函式會傳入IServiceProvider例項,我們的自定義操作也可以參照這個類。
關於Build方法的實現如下,比較簡單主要是呼叫了CreateHandlerPipeline方法:
1: public override HttpMessageHandler Build()
2: {
3: if (PrimaryHandler == null)
4: {
5: var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler));
6: throw new InvalidOperationException(message);
7: }
8:
9: return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers);
10: }
ITypedHttpClientFactory
這是一個抽象工廠,該元件可以使用給定邏輯名稱的自定義配置建立型別化HttpClient例項,與命名方式建立HttpClient具有相同的的功能。型別化客戶端可能用於單個後端終結點,並封裝此終結點的所有處理邏輯。 另一個優勢是它們使用 DI 被注入到應用中需要的位置,下一篇文章會再次討論相關功能。
我們首先看一下呼叫方式:
1: public static IHttpClientBuilder AddHttpClient<TClient>(this IServiceCollection services)
2: where TClient : class
3: {
4: if (services == null)
5: {
6: throw new ArgumentNullException(nameof(services));
7: }
8:
9: AddHttpClient(services);
10:
11: var name = TypeNameHelper.GetTypeDisplayName(typeof(TClient), fullName: false);
12: var builder = new DefaultHttpClientBuilder(services, name);
13: builder.AddTypedClient<TClient>();
14: return builder;
15: }
可以看出此處的呼叫與普通的HttpClient沒有什麼太大區別,只是增加了一個泛型標記,而且該型別沒有特殊的要求,只要是個類就行。其內部依然呼叫AddHttpClient(services),但它呼叫了另一個擴充套件方法,如下所示:
1: public static IHttpClientBuilder AddTypedClient<TClient>(this IHttpClientBuilder builder)
2: where TClient : class
3: {
4: if (builder == null)
5: {
6: throw new ArgumentNullException(nameof(builder));
7: }
8:
9: builder.Services.AddTransient<TClient>(s =>
10: {
11: var httpClientFactory = s.GetRequiredService<IHttpClientFactory>();
12: var httpClient = httpClientFactory.CreateClient(builder.Name);
13:
14: var typedClientFactory = s.GetRequiredService<ITypedHttpClientFactory<TClient>>();
15: return typedClientFactory.CreateClient(httpClient);
16: });
17:
18: return builder;
19: }
可以看到最終的程式碼呼叫了ITypedHttpClientFactory的CreateClient方法,Microsoft.Extensions.Http包中有一個預設的ITypedHttpClientFactory派生類,DefaultTypedHttpClientFactory<TClient>,該類提供了了建構函式用於接收IServiceProvider例項,以及一個內部類宣告的快取物件,該物件十分重要,它被註冊為singleton型別,已達到全域性使用,並可以充當相關例項啟用時的物件池。它也允許它的外部類註冊為transient,這樣它就不會在應用根服務提供程式上被關掉了。
相關程式碼如下:
1: public TClient CreateClient(HttpClient httpClient)
2: {
3: if (httpClient == null)
4: {
5: throw new ArgumentNullException(nameof(httpClient));
6: }
7:
8: return (TClient)_cache.Activator(_services, new object[] { httpClient });
9: }
內部快取物件:
1: public class Cache
2: {
3: private readonly static Func<ObjectFactory> _createActivator = () => ActivatorUtilities.CreateFactory(typeof(TClient), new Type[] { typeof(HttpClient), });
4:
5: private ObjectFactory _activator;
6: private bool _initialized;
7: private object _lock;
8:
9: public ObjectFactory Activator => LazyInitializer.EnsureInitialized(
10: ref _activator,
11: ref _initialized,
12: ref _lock,
13: _createActivator);
14: }
最後我們看一下原始碼中提供的範例:
1: class ExampleClient
2: {
3: private readonly HttpClient _httpClient;
4: private readonly ILogger _logger;
5: // typed clients can use constructor injection to access additional services
6: public ExampleClient(HttpClient httpClient, ILogger<ExampleClient> logger)
7: {
8: _httpClient = httpClient;
9: _logger = logger;
10: }
11: // typed clients can expose the HttpClient for application code to call directly
12: public HttpClient HttpClient => _httpClient;
13: // typed clients can also define methods that abstract usage of the HttpClient
14: public async Task SendHelloRequest()
15: {
16: var response = await _httpClient.GetAsync("/helloworld");
17: response.EnsureSuccessStatusCode();
18: }
19: }
20: //This sample shows how to consume a typed client from an ASP.NET Core middleware.
21: public void Configure(IApplicationBuilder app, ExampleClient exampleClient)
22: {
23: app.Run(async (context) =>
24: {
25: var response = await _exampleClient.GetAsync("/helloworld");
26: await context.Response.WriteAsync("Remote server said: ");
27: await response.Content.CopyToAsync(context.Response.Body);
28: });
29: }
30: //This sample shows how to consume a typed client from an ASP.NET Core MVC Controller.
31: public class HomeController : ControllerBase(IApplicationBuilder app, ExampleClient exampleClient)
32: {
33: private readonly ExampleClient _exampleClient;
34: public HomeController(ExampleClient exampleClient)
35: {
36: _exampleClient = exampleClient;
37: }
38: public async Task<IActionResult> Index()
39: {
40: var response = await _exampleClient.GetAsync("/helloworld");
41: var text = await response.Content.ReadAsStringAsync();
42: return Content("Remote server said: " + text, "text/plain");
43: };
44: }