某些时候我们需要为HttpClient动态配置一些东西, 例如证书等, 参考博问 如何使用IHttpClientFactory动态添加cer证书. 例如服务是一个回调服务, 而被回调方采用了自定义的https(即自定义证书).

上述是一些前情概要, 那么接下来我们就来实现这个需求.

秒想到一个方法, 我们可以直接new HttpClient(), 在每一次要使用的时候都直接来一个, 简单粗暴.

秒想到第二个方法, 又或者用一个Dictionary<string,HttpClient>根据名字缓存client对象.

但是前者性能是个问题,而且涉及到端口的占用释放问题, 在调用量稍大的情况下得凉凉, 后者则是有已知的问题HttpClient对象没法感知dns的变更.

其他一些更不靠谱的方法还有: 使用代码配置方式(services.AddHttpClient("callback provider side").ConfigurePrimaryHttpMessageHandler())配置所有证书, 还有把所有证书都安装的本机上并设置为信任证书.

那么能除了上面这些不靠谱的方式(或者说有致命缺陷的方式), 还有靠谱的么, 那当然是有的, 例如运行时的动态配置实现方案.

所以, 接下来, 我来推荐 2 种方式式,就是我们的IHttpMessageHandlerBuilderFilterIPostConfigureOptions.

官方有什么推荐么?

针对如何为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
  • 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, 很完整的流程图在配上代码, 把它讲解的清清楚楚.

最新文章

  1. html&amp;css中的文字对齐问题
  2. iOS 开发技术牛人博客
  3. Bitset&lt;&gt;用于unordered container时的默认hash函数
  4. Ubuntu 安装OpenCV3.0.0
  5. android OOM分析工具LeakCanary
  6. Microsoft Dynamics CRM2011 必备知识点
  7. 解析 iOS 动画原理与实现
  8. EF学习系列
  9. jQuery 源码分析和使用心得 - 序
  10. sqlite学习笔记4:表的创建和删除
  11. 《Intel汇编第5版》 汇编拷贝字符串
  12. 【Spring】JDBC事务管理XML配置
  13. CentOS7.2安装jdk7u80
  14. 解决javac和java命令在Mac OSX终端里的乱码问题
  15. AS 常用快捷键
  16. spoj QTREE - Query on a tree(树链剖分+线段树单点更新,区间查询)
  17. C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped
  18. SpringMVC由浅入深day02_4springmvc校验
  19. MySQL的ALTER变更、正则查询、分组查询、排序查询以及事务查询的概
  20. linux常用命令:sort 命令

热门文章

  1. &lt;input type=&quot;file&quot; id=&quot;fileID&quot;&gt;文本框里的值清空方法
  2. 【Github搬砖】Python入门网络爬虫之精华版
  3. Mybatis学习之自定义持久层框架(一) 为什么要用框架而不直接用JDBC?
  4. 头文件string.h,cstring与string
  5. NABCD-name not found
  6. 日常Bug排查-系统失去响应-Redis使用不当
  7. 重新整理 .net core 实践篇————配置应用[一]
  8. istio部署问题Q&amp;A
  9. [Java] 数据分析 -- NoSQL数据库
  10. Vim安装记录