<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Architecture on liloew 在写字</title><link>https://blog.liloew.in/tags/architecture/</link><description>Recent content in Architecture on liloew 在写字</description><generator>Hugo -- gohugo.io</generator><lastBuildDate>Sun, 09 Aug 2026 21:42:13 +0800</lastBuildDate><atom:link href="https://blog.liloew.in/tags/architecture/index.xml" rel="self" type="application/rss+xml"/><item><title>从 HTTP 客户端封装看软件架构与编程范式的演进</title><link>https://blog.liloew.in/posts/from-http-client-design-to-architectural-evolution/</link><pubDate>Sun, 09 Aug 2026 21:42:13 +0800</pubDate><guid>https://blog.liloew.in/posts/from-http-client-design-to-architectural-evolution/</guid><description>&lt;h2 id="背景">背景&lt;/h2>
&lt;p>在日常的业务开发中，&lt;code>HTTP&lt;/code> 客户端的封装可以说是最为常见的基础模块，无论是调用第三方 &lt;code>API&lt;/code> 服务、对接内部微服务还是抓取外部数据，几乎每一个项目中都能看到类似于 &lt;code>HttpUtils&lt;/code> 或 &lt;code>HttpClientWrapper&lt;/code> 的身影。然而看似简单的网络请求，在不同的经验层级与架构范式下，写出来的代码质量与设计哲学却有着天壤之别。很多初级的封装仅仅停留在‘把功能调通’的阶段，忽略了底层 &lt;code>TCP&lt;/code> 连接池、资源生命周期管理以及超时控制，为线上高并发场景埋下了严重的性能与稳定性隐患。&lt;/p>
&lt;p>本文将以一个普通的 &lt;code>HTTP&lt;/code> 客户端封装为例一步步演进重构，探讨如何写出完善严谨的代码，并借此梳理软件架构设计与编程范式的变迁。&lt;/p>
&lt;h2 id="第一次封装">第一次封装&lt;/h2>
&lt;p>在一些祖传项目或是更早的基于 &lt;code>JDK 1.7&lt;/code> 以及后来的 &lt;code>JDK 8&lt;/code> 项目中经常可以看到 &lt;code>Apache HttpClient&lt;/code> 类库的身影，以下参考的 &lt;code>HttpUtils&lt;/code> 可能是某个项目上默默工作的最为平凡的封装。&lt;/p></description><content>&lt;h2 id="背景">背景&lt;/h2>
&lt;p>在日常的业务开发中，&lt;code>HTTP&lt;/code> 客户端的封装可以说是最为常见的基础模块，无论是调用第三方 &lt;code>API&lt;/code> 服务、对接内部微服务还是抓取外部数据，几乎每一个项目中都能看到类似于 &lt;code>HttpUtils&lt;/code> 或 &lt;code>HttpClientWrapper&lt;/code> 的身影。然而看似简单的网络请求，在不同的经验层级与架构范式下，写出来的代码质量与设计哲学却有着天壤之别。很多初级的封装仅仅停留在‘把功能调通’的阶段，忽略了底层 &lt;code>TCP&lt;/code> 连接池、资源生命周期管理以及超时控制，为线上高并发场景埋下了严重的性能与稳定性隐患。&lt;/p>
&lt;p>本文将以一个普通的 &lt;code>HTTP&lt;/code> 客户端封装为例一步步演进重构，探讨如何写出完善严谨的代码，并借此梳理软件架构设计与编程范式的变迁。&lt;/p>
&lt;h2 id="第一次封装">第一次封装&lt;/h2>
&lt;p>在一些祖传项目或是更早的基于 &lt;code>JDK 1.7&lt;/code> 以及后来的 &lt;code>JDK 8&lt;/code> 项目中经常可以看到 &lt;code>Apache HttpClient&lt;/code> 类库的身影，以下参考的 &lt;code>HttpUtils&lt;/code> 可能是某个项目上默默工作的最为平凡的封装。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Java" data-lang="Java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HttpUtils&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> Logger log &lt;span style="color:#f92672">=&lt;/span> LoggerFactory&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getLogger&lt;/span>&lt;span style="color:#f92672">(&lt;/span>HttpUtils&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">class&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> String &lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CloseableHttpClient httpClient &lt;span style="color:#f92672">=&lt;/span> HttpClients&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">createDefault&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpGet httpGet &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HttpGet&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>headers &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">forEach&lt;/span>&lt;span style="color:#f92672">(&lt;/span>httpGet&lt;span style="color:#f92672">::&lt;/span>addHeader&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CloseableHttpResponse response &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response &lt;span style="color:#f92672">=&lt;/span> httpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">(&lt;/span>httpGet&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getStatusLine&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">getStatusCode&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">200&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpEntity entity &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getEntity&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> entity &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span> &lt;span style="color:#f92672">?&lt;/span> EntityUtils&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">toString&lt;/span>&lt;span style="color:#f92672">(&lt;/span>entity&lt;span style="color:#f92672">,&lt;/span> StandardCharsets&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">UTF_8&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">else&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;GET 请求失败 - URL: {} Status Code: {}&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">,&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getStatusLine&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">getStatusCode&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">catch&lt;/span> &lt;span style="color:#f92672">(&lt;/span>Exception e&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;GET 请求发生异常 - URL: {} Exception: &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">,&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">finally&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">close&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">catch&lt;/span> &lt;span style="color:#f92672">(&lt;/span>IOException e&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;关闭 response 失败&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> String &lt;span style="color:#a6e22e">doPostJson&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> String jsonPayload&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CloseableHttpClient httpClient &lt;span style="color:#f92672">=&lt;/span> HttpClients&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">createDefault&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpPost httpPost &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HttpPost&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpPost&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">setHeader&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Content-Type&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;application/json;charset=UTF-8&amp;#34;&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CloseableHttpResponse response &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>jsonPayload &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpPost&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">setEntity&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">new&lt;/span> StringEntity&lt;span style="color:#f92672">(&lt;/span>jsonPayload&lt;span style="color:#f92672">,&lt;/span> StandardCharsets&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">UTF_8&lt;/span>&lt;span style="color:#f92672">));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response &lt;span style="color:#f92672">=&lt;/span> httpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">(&lt;/span>httpPost&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getStatusLine&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">getStatusCode&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#ae81ff">200&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> EntityUtils&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">toString&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getEntity&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> StandardCharsets&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">UTF_8&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">catch&lt;/span> &lt;span style="color:#f92672">(&lt;/span>Exception e&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;POST 请求发生异常 - URL: {} Exception: &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> url&lt;span style="color:#f92672">,&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">throw&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> RuntimeException&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;HTTP POST Failed&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">finally&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">close&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">catch&lt;/span> &lt;span style="color:#f92672">(&lt;/span>IOException e&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;关闭 response 失败&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这段代码逻辑并不复杂，可以说有着三到五年经验的 &lt;code>Java&lt;/code> 开发者都能写出来。同时相信比较有经验的开发者一眼便可以看出这段代码中存在一些比较明显问题。首先是&lt;!-- raw HTML omitted -->资源的频繁创建与销毁，&lt;!-- raw HTML omitted -->每次调用 &lt;code>doGet&lt;/code> 或 &lt;code>doPostJson&lt;/code> 都会执行 &lt;code>HttpClients.createDefault()&lt;/code>，由于没有使用单例或连接池（&lt;code>PoolingHttpClientConnectionManager&lt;/code>），在高并发场景下会导致大量 &lt;code>TIME_WAIT&lt;/code> 状态的 TCP 连接；其次是&lt;!-- raw HTML omitted -->资源没有正确释放，&lt;!-- raw HTML omitted -->&lt;code>CloseableHttpClient&lt;/code> 本身实现了 &lt;code>Closeable&lt;/code> 接口，但代码中没有关闭 &lt;code>httpClient&lt;/code>，很容易导致 &lt;code>HTTP&lt;/code> 连接无法释放；另外就是&lt;!-- raw HTML omitted -->灵活性差，&lt;!-- raw HTML omitted -->没有暴露超时等关键参数，全局采用默认配置，面对不稳定的下游服务时会造成线程卡死。当然还有其他一些问题在此处不再一一列举。&lt;/p>
&lt;h2 id="通过更好的类库进行改进">通过更好的类库进行改进&lt;/h2>
&lt;p>考虑到 &lt;code>Apache HttpClients&lt;/code> 这个库诞生已经很久了不能说跟不上目前 &lt;code>Java&lt;/code> 的发展趋势倒也可以说在针对 &lt;code>HTTP&lt;/code> 等场景出现了很多更好的方案。为了解决以上问题同时契合现代化 &lt;code>Java&lt;/code> 开发，接下来引入 &lt;code>okHttp&lt;/code> 作为底层接入库。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Java" data-lang="Java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">SmartHttpClient&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> Logger log &lt;span style="color:#f92672">=&lt;/span> LoggerFactory&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getLogger&lt;/span>&lt;span style="color:#f92672">(&lt;/span>SmartHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">class&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> OkHttpClient okHttpClient&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">SmartHttpClient&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">okHttpClient&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> OkHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">connectTimeout&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">SECONDS&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">readTimeout&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">SECONDS&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">writeTimeout&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">SECONDS&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">connectionPool&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">new&lt;/span> ConnectionPool&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">32&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">MINUTES&lt;/span>&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> record &lt;span style="color:#a6e22e">HttpResponse&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">int&lt;/span> statusCode&lt;span style="color:#f92672">,&lt;/span> String body&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">boolean&lt;/span> &lt;span style="color:#a6e22e">isSuccessful&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> statusCode &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">200&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> statusCode &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">300&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">HttpClientException&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> Exception &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">int&lt;/span> statusCode&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">HttpClientException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String message&lt;span style="color:#f92672">,&lt;/span> Throwable cause&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">super&lt;/span>&lt;span style="color:#f92672">(&lt;/span>message&lt;span style="color:#f92672">,&lt;/span> cause&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">statusCode&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">-&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">HttpClientException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">int&lt;/span> statusCode&lt;span style="color:#f92672">,&lt;/span> String message&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">super&lt;/span>&lt;span style="color:#f92672">(&lt;/span>message&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">statusCode&lt;/span> &lt;span style="color:#f92672">=&lt;/span> statusCode&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">int&lt;/span> &lt;span style="color:#a6e22e">getStatusCode&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> statusCode&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> HttpResponse &lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">throws&lt;/span> HttpClientException &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span> requestBuilder &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">get&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>headers &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">forEach&lt;/span>&lt;span style="color:#f92672">(&lt;/span>requestBuilder&lt;span style="color:#f92672">::&lt;/span>addHeader&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> execute&lt;span style="color:#f92672">(&lt;/span>requestBuilder&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> HttpResponse &lt;span style="color:#a6e22e">doPostJson&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> String jsonPayload&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">throws&lt;/span> HttpClientException &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RequestBody body &lt;span style="color:#f92672">=&lt;/span> RequestBody&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">create&lt;/span>&lt;span style="color:#f92672">(&lt;/span>jsonPayload&lt;span style="color:#f92672">,&lt;/span> MediaType&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">parse&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;application/json; charset=utf-8&amp;#34;&lt;/span>&lt;span style="color:#f92672">));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span> requestBuilder &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">post&lt;/span>&lt;span style="color:#f92672">(&lt;/span>body&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>headers &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">forEach&lt;/span>&lt;span style="color:#f92672">(&lt;/span>requestBuilder&lt;span style="color:#f92672">::&lt;/span>addHeader&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> execute&lt;span style="color:#f92672">(&lt;/span>requestBuilder&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> HttpResponse &lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">(&lt;/span>Request request&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">throws&lt;/span> HttpClientException &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">(&lt;/span>Response response &lt;span style="color:#f92672">=&lt;/span> okHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">newCall&lt;/span>&lt;span style="color:#f92672">(&lt;/span>request&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">())&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> statusCode &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">code&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> String responseBody &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResponseBody body &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>body &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> responseBody &lt;span style="color:#f92672">=&lt;/span> body&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">string&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(!&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">isSuccessful&lt;/span>&lt;span style="color:#f92672">())&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">warn&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;HTTP 请求未成功 - Status: {} URL: {}&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> statusCode&lt;span style="color:#f92672">,&lt;/span> request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headersMap &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HashMap&lt;span style="color:#f92672">&amp;lt;&amp;gt;();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">headers&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">forEach&lt;/span>&lt;span style="color:#f92672">(&lt;/span>pair &lt;span style="color:#f92672">-&amp;gt;&lt;/span> headersMap&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">put&lt;/span>&lt;span style="color:#f92672">(&lt;/span>pair&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getFirst&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> pair&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getSecond&lt;/span>&lt;span style="color:#f92672">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HttpResponse&lt;span style="color:#f92672">(&lt;/span>statusCode&lt;span style="color:#f92672">,&lt;/span> responseBody&lt;span style="color:#f92672">,&lt;/span> headersMap&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">catch&lt;/span> &lt;span style="color:#f92672">(&lt;/span>IOException e&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;HTTP 请求发生 IO 异常 - URL: {} Exception: &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">throw&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HttpClientException&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Network execution failed for URL: &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> e&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以上代码首先通过&lt;strong>单例模式&lt;/strong> 的 &lt;code>OkHttpClient&lt;/code>复用底层的 TCP 连接池与线程池，其内部的 &lt;code>ConnectionPool&lt;/code> 和 &lt;code>Dispatcher&lt;/code> 被所有请求共享可以有效避免高并发下频繁建连导致的 TCP &lt;code>TIME_WAIT&lt;/code> 和 &lt;code>Socket&lt;/code> 耗尽等问题；其次利用 &lt;code>try-with-resources&lt;/code> 语法，确保 &lt;code>ResponseBody&lt;/code> 不论是正常返回还是发生异常，底层的 &lt;code>ResponseBody&lt;/code> 都能被及时关闭并归还连接，避免了&lt;strong>显式资源管理&lt;/strong>；另外引入了自定义的 Response 结构体与强类型异常，不再返回模糊的 &lt;code>null&lt;/code>，让调用方感知 HTTP 状态码与网络层故障；同时通过为 &lt;code>OkHttpClient&lt;/code> 配置 &lt;code>Interceptor&lt;/code>，可以在不修改主逻辑的前提下统一实现全局链路日志记录、通用 Header 注入、重试等功能。&lt;/p>
&lt;p>关于向上抛出异常与传递错误信息这一点在很多内部封装的二方包中处理的不太好进而导致掩盖了一些问题和线索，作为二方包应当将最原始的错误信息暴露给应用层或调用方。&lt;/p>
&lt;h2 id="引入不可变性及函数式思维">引入不可变性及函数式思维&lt;/h2>
&lt;p>上一版本中由于依赖 &lt;code>throws HttpClientException&lt;/code> 控制错误流程，在复杂的业务链路中受检异常会导致方法签名层层传染或是导致开发者在 &lt;code>Lambda&lt;/code> 表达式中不得不编写丑陋的 &lt;code>try-catch&lt;/code> 块。同时在 &lt;code>execute()&lt;/code> 方法包含了网络 &lt;code>I/O&lt;/code> 副作用，但在方法签名层面却和普通的纯内存计算无异，上层调用者无法通过类型系统直观看出这个操作是否包含副作用以及可能的错误分支。&lt;/p>
&lt;p>接下来我们在前一版本的基础上通过引入 &lt;code>Vavr&lt;/code> 这个函数式库继续完善，不再依靠抛出 &lt;code>Exception&lt;/code> 控制流程也不再返回可能隐藏问题的裸对象。我们将 &lt;code>可能失败的计算&lt;/code> 与 &lt;code>显式的错误类型&lt;/code> 提升为一等公民 &lt;code>First-class Citizen&lt;/code>，利用 &lt;strong>Monad（单子）&lt;/strong> 模式实现声明式的链式组合。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Java" data-lang="Java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">FnHttpClient&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> OkHttpClient okHttpClient&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#a6e22e">FnHttpClient&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">this&lt;/span>&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">okHttpClient&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> OkHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">connectTimeout&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">SECONDS&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">readTimeout&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">10&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">SECONDS&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">connectionPool&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">new&lt;/span> ConnectionPool&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#ae81ff">32&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">,&lt;/span> TimeUnit&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">MINUTES&lt;/span>&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> record &lt;span style="color:#a6e22e">HttpResponse&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">int&lt;/span> statusCode&lt;span style="color:#f92672">,&lt;/span> String body&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">boolean&lt;/span> &lt;span style="color:#a6e22e">isSuccess&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> statusCode &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">200&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> statusCode &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">300&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> sealed &lt;span style="color:#66d9ef">interface&lt;/span> &lt;span style="color:#a6e22e">HttpError&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> record &lt;span style="color:#a6e22e">NetworkError&lt;/span>&lt;span style="color:#f92672">(&lt;/span>Throwable cause&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">implements&lt;/span> HttpError &lt;span style="color:#f92672">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> record &lt;span style="color:#a6e22e">StatusError&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">int&lt;/span> statusCode&lt;span style="color:#f92672">,&lt;/span> String body&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">implements&lt;/span> HttpError &lt;span style="color:#f92672">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>HttpError&lt;span style="color:#f92672">,&lt;/span> HttpResponse&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span> builder &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">get&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> optionallyAddHeaders&lt;span style="color:#f92672">(&lt;/span>builder&lt;span style="color:#f92672">,&lt;/span> headers&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> execute&lt;span style="color:#f92672">(&lt;/span>builder&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>HttpError&lt;span style="color:#f92672">,&lt;/span> HttpResponse&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">doPostJson&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String url&lt;span style="color:#f92672">,&lt;/span> String jsonPayload&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RequestBody body &lt;span style="color:#f92672">=&lt;/span> RequestBody&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">create&lt;/span>&lt;span style="color:#f92672">(&lt;/span>jsonPayload&lt;span style="color:#f92672">,&lt;/span> MediaType&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">parse&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;application/json; charset=utf-8&amp;#34;&lt;/span>&lt;span style="color:#f92672">));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span> builder &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">url&lt;/span>&lt;span style="color:#f92672">(&lt;/span>url&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">post&lt;/span>&lt;span style="color:#f92672">(&lt;/span>body&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> otionallyAddHeaders&lt;span style="color:#f92672">(&lt;/span>builder&lt;span style="color:#f92672">,&lt;/span> headers&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> execute&lt;span style="color:#f92672">(&lt;/span>builder&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">build&lt;/span>&lt;span style="color:#f92672">());&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>HttpError&lt;span style="color:#f92672">,&lt;/span> HttpResponse&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">(&lt;/span>Request request&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> Try&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">of&lt;/span>&lt;span style="color:#f92672">(()&lt;/span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span> executePhysicalCall&lt;span style="color:#f92672">(&lt;/span>request&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">toEither&lt;/span>&lt;span style="color:#f92672">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">mapLeft&lt;/span>&lt;span style="color:#f92672">(&lt;/span>HttpError&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">NetworkError&lt;/span>&lt;span style="color:#f92672">::&lt;/span>&lt;span style="color:#66d9ef">new&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">flatMap&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">-&amp;gt;&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">isSuccess&lt;/span>&lt;span style="color:#f92672">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">?&lt;/span> Either&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">right&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">:&lt;/span> Either&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">left&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">new&lt;/span> HttpError&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">StatusError&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">statusCode&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">())));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> HttpResponse &lt;span style="color:#a6e22e">executePhysicalCall&lt;/span>&lt;span style="color:#f92672">(&lt;/span>Request request&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">throws&lt;/span> Exception &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">try&lt;/span> &lt;span style="color:#f92672">(&lt;/span>Response response &lt;span style="color:#f92672">=&lt;/span> okHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">newCall&lt;/span>&lt;span style="color:#f92672">(&lt;/span>request&lt;span style="color:#f92672">).&lt;/span>&lt;span style="color:#a6e22e">execute&lt;/span>&lt;span style="color:#f92672">())&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">int&lt;/span> code &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">code&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResponseBody body &lt;span style="color:#f92672">=&lt;/span> response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> String responseStr &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">(&lt;/span>body &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">?&lt;/span> body&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">string&lt;/span>&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>&lt;span style="color:#f92672">;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headersMap &lt;span style="color:#f92672">=&lt;/span> HashMap&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">ofAll&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">headers&lt;/span>&lt;span style="color:#f92672">().&lt;/span>&lt;span style="color:#a6e22e">toMultimap&lt;/span>&lt;span style="color:#f92672">())&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">mapValues&lt;/span>&lt;span style="color:#f92672">(&lt;/span>list &lt;span style="color:#f92672">-&amp;gt;&lt;/span> String&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">join&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;, &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> list&lt;span style="color:#f92672">));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> HttpResponse&lt;span style="color:#f92672">(&lt;/span>code&lt;span style="color:#f92672">,&lt;/span> responseStr&lt;span style="color:#f92672">,&lt;/span> headersMap&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">void&lt;/span> &lt;span style="color:#a6e22e">otionallyAddHeaders&lt;/span>&lt;span style="color:#f92672">(&lt;/span>Request&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">Builder&lt;/span> builder&lt;span style="color:#f92672">,&lt;/span> Map&lt;span style="color:#f92672">&amp;lt;&lt;/span>String&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">(&lt;/span>headers &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">null&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#f92672">!&lt;/span>headers&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">isEmpty&lt;/span>&lt;span style="color:#f92672">())&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> headers&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">forEach&lt;/span>&lt;span style="color:#f92672">(&lt;/span>builder&lt;span style="color:#f92672">::&lt;/span>addHeader&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>首先使用 &lt;code>Vavr&lt;/code> 的 &lt;code>Try&amp;lt;T&amp;gt;&lt;/code> 容器包裹网络 &lt;code>I/O&lt;/code> 副作用将异常转化为可传输的值避免了上层应用需要显式处理受检异常；其次使用 &lt;code>Either&amp;lt;HttpError, HttpResponse&amp;gt;&lt;/code> 强类型清晰区分是连接超时或 &lt;code>DNS&lt;/code> 解析错误等网络系统层面故障还是业务层或 &lt;code>401&lt;/code>、&lt;code>500&lt;/code> 等 &lt;code>HTTP&lt;/code> 协议层错误；&lt;/p>
&lt;p>同时还解决了如果业务需要 &lt;code>发起请求 A&lt;/code> -&amp;gt; &lt;code>拿到结果解析后发起请求 B&lt;/code> -&amp;gt; &lt;code>失败则降级到请求 C&lt;/code>，在 &lt;code>Java&lt;/code> 的指令式风格下代码会退化为层层嵌套的 &lt;code>if-else&lt;/code> 或 &lt;code>try-catch&lt;/code>，无法以声明式的方式进行组合问题。以下代码片段则展示了如何通过在 &lt;code>Either&lt;/code> 类型上的 &lt;code>Monad&lt;/code> 算子 (&lt;code>flatMap&lt;/code>) 实现声明式的链式管道调用优雅地表达整个流程：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">public&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">BusinessWorkflowExample&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">static&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> Logger log &lt;span style="color:#f92672">=&lt;/span> LoggerFactory&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getLogger&lt;/span>&lt;span style="color:#f92672">(&lt;/span>BusinessWorkflowExample&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">class&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">final&lt;/span> FnHttpClient httpClient &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> FnHttpClient&lt;span style="color:#f92672">();&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>FnHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">HttpError&lt;/span>&lt;span style="color:#f92672">,&lt;/span> UserDetail&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">getUserWorkflow&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String userId&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 获取 Token
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">return&lt;/span> fetchToken&lt;span style="color:#f92672">(&lt;/span>userId&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 拿到 Token 后解析并发起请求 B
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">flatMap&lt;/span>&lt;span style="color:#f92672">(&lt;/span>token &lt;span style="color:#f92672">-&amp;gt;&lt;/span> fetchUserDetail&lt;span style="color:#f92672">(&lt;/span>userId&lt;span style="color:#f92672">,&lt;/span> token&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 如果请求 B 失败降级执行请求 C
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">recoverWith&lt;/span>&lt;span style="color:#f92672">(&lt;/span>error &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">warn&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;请求 B 失败, 启动降级逻辑调用请求 C - Exception: &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> error&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> fetchGuestUserDetail&lt;span style="color:#f92672">(&lt;/span>userId&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">});&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * 请求 A 获取 Token
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>FnHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">HttpError&lt;/span>&lt;span style="color:#f92672">,&lt;/span> String&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">fetchToken&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String userId&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> httpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://api.example.com/v1/auth/token?userId=&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> userId&lt;span style="color:#f92672">,&lt;/span> HashMap&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">empty&lt;/span>&lt;span style="color:#f92672">())&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// map 会自动处理 Either 的 Right 情况, 如果是 Left 则直接返回
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">map&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">-&amp;gt;&lt;/span> parseTokenFromJson&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * 请求 B 根据 Token 获取用户详情
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>FnHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">HttpError&lt;/span>&lt;span style="color:#f92672">,&lt;/span> UserDetail&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">fetchUserDetail&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String userId&lt;span style="color:#f92672">,&lt;/span> String token&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var headers &lt;span style="color:#f92672">=&lt;/span> HashMap&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">of&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;Authorization&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;Bearer &amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> token&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> httpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://api.example.com/v1/users/&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> userId&lt;span style="color:#f92672">,&lt;/span> headers&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">map&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">-&amp;gt;&lt;/span> parseUserDetailFromJson&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/**
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * 请求 C 降级接口 - 备用游客模式
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>FnHttpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">HttpError&lt;/span>&lt;span style="color:#f92672">,&lt;/span> UserDetail&lt;span style="color:#f92672">&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">fetchGuestUserDetail&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String userId&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> httpClient&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">doGet&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;https://backup-api.example.com/v1/guest-users/&amp;#34;&lt;/span> &lt;span style="color:#f92672">+&lt;/span> userId&lt;span style="color:#f92672">,&lt;/span> HashMap&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">empty&lt;/span>&lt;span style="color:#f92672">())&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">map&lt;/span>&lt;span style="color:#f92672">(&lt;/span>response &lt;span style="color:#f92672">-&amp;gt;&lt;/span> parseUserDetailFromJson&lt;span style="color:#f92672">(&lt;/span>response&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">body&lt;/span>&lt;span style="color:#f92672">()));&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">public&lt;/span> record &lt;span style="color:#a6e22e">UserDetail&lt;/span>&lt;span style="color:#f92672">(&lt;/span>String id&lt;span style="color:#f92672">,&lt;/span> String name&lt;span style="color:#f92672">,&lt;/span> String type&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">{}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这样无论内部发生了多少次请求拼接和降级，上层业务方拿到的是一个干净的 &lt;code>Either&lt;/code> 结果可以通过 &lt;code>fold&lt;/code> 实现一站式消费：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-java" data-lang="java">&lt;span style="display:flex;">&lt;span>Either&lt;span style="color:#f92672">&amp;lt;&lt;/span>HttpError&lt;span style="color:#f92672">,&lt;/span> UserDetail&lt;span style="color:#f92672">&amp;gt;&lt;/span> result &lt;span style="color:#f92672">=&lt;/span> workflow&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">getUserWorkflow&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;someUser&amp;#34;&lt;/span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>result&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">fold&lt;/span>&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error &lt;span style="color:#f92672">-&amp;gt;&lt;/span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">error&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;流程终极失败, 链路中所有重试降级均已耗尽 - Exception: &amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> error&lt;span style="color:#f92672">),&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> detail &lt;span style="color:#f92672">-&amp;gt;&lt;/span> log&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">info&lt;/span>&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;成功获取用户详情 - name: {} type: {}&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> detail&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">name&lt;/span>&lt;span style="color:#f92672">(),&lt;/span> detail&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">type&lt;/span>&lt;span style="color:#f92672">())&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="完全的函数式封装">完全的函数式封装&lt;/h2>
&lt;p>接下来我们使用 &lt;code>Scala&lt;/code> 来进一步完善 &lt;code>HTTP&lt;/code> 客户端封装。考虑到很多 &lt;code>RESTful&lt;/code> 服务需要进行鉴权，因此我们首先实现一个基于 &lt;code>Cats Effect&lt;/code> 的并发 &lt;code>Token&lt;/code> 管理器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Scala" data-lang="Scala">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">TokenState&lt;/span>&lt;span style="color:#f92672">(&lt;/span>token&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">,&lt;/span> expiresAt&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">FiniteDuration&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">trait&lt;/span> &lt;span style="color:#a6e22e">TokenProvider&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]]&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> getToken&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> invalidate&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Unit&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#75715e">// 收到 401 时调用, 强制下次重新获取
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">object&lt;/span> &lt;span style="color:#a6e22e">TokenProvider&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> make&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Async&lt;/span>&lt;span style="color:#f92672">](&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fetchToken&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">],&lt;/span> &lt;span style="color:#75715e">// 实际调用鉴权接口获取新 token 的逻辑
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> safetyMargin&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">FiniteDuration&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">30.&lt;/span>seconds &lt;span style="color:#75715e">// 提前多久算过期(默认 30s), 避免临界点问题
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenProvider&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">]]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ref &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Ref&lt;/span>&lt;span style="color:#f92672">.&lt;/span>of&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>, &lt;span style="color:#66d9ef">Option&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">]](&lt;/span>&lt;span style="color:#a6e22e">None&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sem &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Semaphore&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">](&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">// 保证同一时刻只有一个请求在刷新
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">yield&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">TokenProvider&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">def&lt;/span> isExpired&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">,&lt;/span> now&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">FiniteDuration&lt;/span>&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Boolean&lt;/span> &lt;span style="color:#f92672">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> now &lt;span style="color:#f92672">&amp;gt;=&lt;/span> s&lt;span style="color:#f92672">.&lt;/span>expiresAt &lt;span style="color:#f92672">-&lt;/span> safetyMargin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">def&lt;/span> refresh&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sem&lt;span style="color:#f92672">.&lt;/span>permit&lt;span style="color:#f92672">.&lt;/span>use &lt;span style="color:#f92672">{&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> now &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Clock&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>monotonic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> ref&lt;span style="color:#f92672">.&lt;/span>get
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 双重检查 - 拿到锁后再判断一次, 避免重复刷新
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> state &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> cur &lt;span style="color:#66d9ef">match&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Some&lt;/span>&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">!&lt;/span>isExpired&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">,&lt;/span> now&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> fetchToken&lt;span style="color:#f92672">.&lt;/span>flatTap&lt;span style="color:#f92672">(&lt;/span>s &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> ref&lt;span style="color:#f92672">.&lt;/span>set&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">Some&lt;/span>&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">)))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">yield&lt;/span> state
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> getToken&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> now &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Clock&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>monotonic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cur &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> ref&lt;span style="color:#f92672">.&lt;/span>get
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> state &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> cur &lt;span style="color:#66d9ef">match&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Some&lt;/span>&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#f92672">!&lt;/span>isExpired&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">,&lt;/span> now&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>s&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> refresh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">yield&lt;/span> state&lt;span style="color:#f92672">.&lt;/span>token
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> invalidate&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Unit&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> ref&lt;span style="color:#f92672">.&lt;/span>set&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">None&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里我们在定义用于存储 &lt;code>Token&lt;/code> 的 &lt;code>TokenState&lt;/code> 时选择使用&lt;strong>单调时钟&lt;/strong>（&lt;code>Monotonic Clock&lt;/code>）代替&lt;strong>墙上时钟&lt;/strong>（&lt;code>Wall-clock&lt;/code>）可以避免系统时间回拨引起的 &lt;code>Token&lt;/code> 提前失效或永不失效问题。我们只需在获取 &lt;code>Token&lt;/code> 时记录当前的单调时间，并加上 &lt;code>expires_in&lt;/code> 的偏移量，即可安全判断过期状态。值得一提的是这里的 &lt;code>F[_]&lt;/code> 并非普通的泛型而是 &lt;code>Scala&lt;/code> 中特有的 &lt;code>高阶类型&lt;/code>，&lt;code>F&lt;/code> 自身并不是一个完整的类型且必须接受一个类型参数才能变成具体类型，从而将具体实现交给调用方选择是使用 &lt;code>Scala&lt;/code> 标准库的 &lt;code>Future&lt;/code> 还是 &lt;code>Cats Effect&lt;/code> 的 &lt;code>IO&lt;/code> 抑或是 &lt;code>ZIO&lt;/code> 等具体实现。&lt;/p>
&lt;p>在定义好 &lt;code>TokenProvider&lt;/code> 后接下来就是定义具体如何获取 &lt;code>Token&lt;/code> 的 &lt;code>TokenFetcher&lt;/code> 了：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-scala" data-lang="scala">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">OAuthConfig&lt;/span>&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tokenUrl&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">sttp.model.Uri&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clientId&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clientSecret&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> scope&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Option&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">None&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> extraParams&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Map&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">String&lt;/span>, &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">Map&lt;/span>&lt;span style="color:#f92672">.&lt;/span>empty
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// 携带 HTTP 状态码，方便区分哪些错误值得重试
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">TokenFetchException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>status&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Int&lt;/span>&lt;span style="color:#f92672">,&lt;/span> message&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#a6e22e">RuntimeException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>message&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">object&lt;/span> &lt;span style="color:#a6e22e">TokenFetcher&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#a6e22e">LazyLogging&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 真正发起 HTTP 请求获取 token 的逻辑（不含重试）
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">def&lt;/span> rawFetchToken&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Async&lt;/span>&lt;span style="color:#f92672">](&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> backend&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Backend&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">],&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> config&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">OAuthConfig&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">val&lt;/span> body &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">Map&lt;/span>&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;grant_type&amp;#34;&lt;/span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;client_credentials&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;client_id&amp;#34;&lt;/span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>clientId&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;client_secret&amp;#34;&lt;/span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>clientSecret
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">++&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>scope&lt;span style="color:#f92672">.&lt;/span>map&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;scope&amp;#34;&lt;/span> &lt;span style="color:#f92672">-&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#f92672">++&lt;/span> config&lt;span style="color:#f92672">.&lt;/span>extraParams
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resp &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> basicRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>post&lt;span style="color:#f92672">(&lt;/span>config&lt;span style="color:#f92672">.&lt;/span>tokenUrl&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>body&lt;span style="color:#f92672">(&lt;/span>body&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>response&lt;span style="color:#f92672">(&lt;/span>asJson&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenResponse&lt;/span>&lt;span style="color:#f92672">])&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>send&lt;span style="color:#f92672">(&lt;/span>backend&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> now &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Clock&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>monotonic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> state &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> resp&lt;span style="color:#f92672">.&lt;/span>body &lt;span style="color:#66d9ef">match&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Right&lt;/span>&lt;span style="color:#f92672">(&lt;/span>tr&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">TokenState&lt;/span>&lt;span style="color:#f92672">(&lt;/span>tr&lt;span style="color:#f92672">.&lt;/span>accessToken&lt;span style="color:#f92672">,&lt;/span> now &lt;span style="color:#f92672">+&lt;/span> tr&lt;span style="color:#f92672">.&lt;/span>expiresIn&lt;span style="color:#f92672">.&lt;/span>seconds&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Left&lt;/span>&lt;span style="color:#f92672">(&lt;/span>err&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>raiseError&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">](&lt;/span>&lt;span style="color:#a6e22e">TokenFetchException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>resp&lt;span style="color:#f92672">.&lt;/span>code&lt;span style="color:#f92672">.&lt;/span>code&lt;span style="color:#f92672">,&lt;/span> err&lt;span style="color:#f92672">.&lt;/span>getMessage&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">yield&lt;/span> state
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// 判断哪些异常值得重试: 网络抖动, 超时, 5xx 需要重试; 4xx(凭证错误等配置问题)不重试
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">def&lt;/span> isRetriable&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Async&lt;/span>&lt;span style="color:#f92672">](&lt;/span>e&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Throwable&lt;/span>&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Boolean&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> e &lt;span style="color:#66d9ef">match&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">_:&lt;/span> &lt;span style="color:#66d9ef">java.net.ConnectException&lt;/span> &lt;span style="color:#f92672">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">true&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">_:&lt;/span> &lt;span style="color:#66d9ef">java.util.concurrent.TimeoutException&lt;/span> &lt;span style="color:#f92672">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">true&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">TokenFetchException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>status&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> status &lt;span style="color:#f92672">&amp;gt;=&lt;/span> &lt;span style="color:#ae81ff">500&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">true&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">TokenFetchException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>status&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> status &lt;span style="color:#f92672">&amp;lt;&lt;/span> &lt;span style="color:#ae81ff">500&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">false&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#66d9ef">_&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">true&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#75715e">// 未知异常保守重试
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** 构造带重试/退避策略的 fetchToken，直接传给 TokenProvider.make */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> withRetry&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Async&lt;/span>&lt;span style="color:#f92672">](&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> backend&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Backend&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">],&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> config&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">OAuthConfig&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> maxRetries&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Int&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">5&lt;/span>&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> baseDelay&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">FiniteDuration&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">200.&lt;/span>millis&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> maxDelay&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">FiniteDuration&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#ae81ff">5.&lt;/span>seconds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// fullJitter：指数退避基础上加随机抖动，避免多实例同步重试打爆鉴权服务
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> basePolicy &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">RetryPolicies&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>fullJitter&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">](&lt;/span>baseDelay&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>join&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">RetryPolicies&lt;/span>&lt;span style="color:#f92672">.&lt;/span>limitRetries&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">](&lt;/span>maxRetries&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">val&lt;/span> policy &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">RetryPolicies&lt;/span>&lt;span style="color:#f92672">.&lt;/span>capDelay&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">](&lt;/span>maxDelay&lt;span style="color:#f92672">,&lt;/span> basePolicy&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> retryingOnSomeErrors&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TokenState&lt;/span>&lt;span style="color:#f92672">](&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy &lt;span style="color:#66d9ef">=&lt;/span> policy&lt;span style="color:#f92672">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isWorthRetrying &lt;span style="color:#66d9ef">=&lt;/span> isRetriable&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">],&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> onError &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#f92672">(&lt;/span>e&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Throwable&lt;/span>&lt;span style="color:#f92672">,&lt;/span> details&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">RetryDetails&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>delay&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger&lt;span style="color:#f92672">.&lt;/span>info&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">s&amp;#34;[TokenFetcher] fetch token failed (&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>details&lt;span style="color:#f92672">.&lt;/span>retriesSoFar&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> retries so far), retrying... error=&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>e&lt;span style="color:#f92672">.&lt;/span>getMessage&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)(&lt;/span>rawFetchToken&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">](&lt;/span>backend&lt;span style="color:#f92672">,&lt;/span> config&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>接下来是业务层或调用方根据实际业务场景定义具体的业务接口：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-scala" data-lang="scala">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">final&lt;/span> &lt;span style="color:#66d9ef">class&lt;/span> &lt;span style="color:#a6e22e">APIClient&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">_&lt;/span>&lt;span style="color:#f92672">]&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Async&lt;/span>&lt;span style="color:#f92672">](&lt;/span>backend&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Backend&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">],&lt;/span> baseUrl&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Uri&lt;/span>&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">extends&lt;/span> &lt;span style="color:#a6e22e">LazyLogging&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** 获取任务列表 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> listTasks&lt;span style="color:#f92672">()&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">List&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TaskSummary&lt;/span>&lt;span style="color:#f92672">]]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> basicRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>get&lt;span style="color:#f92672">(&lt;/span>baseUrl&lt;span style="color:#f92672">.&lt;/span>addPath&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;api&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;tasks&amp;#34;&lt;/span>&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>response&lt;span style="color:#f92672">(&lt;/span>asJson&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">List&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TaskSummary&lt;/span>&lt;span style="color:#f92672">]])&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>send&lt;span style="color:#f92672">(&lt;/span>backend&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>flatMap&lt;span style="color:#f92672">(&lt;/span>handleResponse&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** 获取任务详情 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> getTaskDetail&lt;span style="color:#f92672">(&lt;/span>taskId&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TaskDetail&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> basicRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>get&lt;span style="color:#f92672">(&lt;/span>baseUrl&lt;span style="color:#f92672">.&lt;/span>addPath&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;api&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;tasks&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> taskId&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>response&lt;span style="color:#f92672">(&lt;/span>asJson&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">TaskDetail&lt;/span>&lt;span style="color:#f92672">])&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>send&lt;span style="color:#f92672">(&lt;/span>backend&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>flatMap&lt;span style="color:#f92672">(&lt;/span>handleResponse&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> uploadFile&lt;span style="color:#f92672">(&lt;/span>taskId&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">,&lt;/span> fileName&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">,&lt;/span> fileBytes&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Array&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Byte&lt;/span>&lt;span style="color:#f92672">])&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">UploadResult&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> basicRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>post&lt;span style="color:#f92672">(&lt;/span>baseUrl&lt;span style="color:#f92672">.&lt;/span>addPath&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;api&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;tasks&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> taskId&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#e6db74">&amp;#34;files&amp;#34;&lt;/span>&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>multipartBody&lt;span style="color:#f92672">(&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> multipart&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">&amp;#34;file&amp;#34;&lt;/span>&lt;span style="color:#f92672">,&lt;/span> fileBytes&lt;span style="color:#f92672">).&lt;/span>fileName&lt;span style="color:#f92672">(&lt;/span>fileName&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>response&lt;span style="color:#f92672">(&lt;/span>asJson&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">UploadResult&lt;/span>&lt;span style="color:#f92672">])&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>send&lt;span style="color:#f92672">(&lt;/span>backend&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>flatMap&lt;span style="color:#f92672">(&lt;/span>handleResponse&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** 统一处理响应：成功则取出解析好的 body 失败则包装成异常抛出 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">def&lt;/span> handleResponse&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">T&lt;/span>&lt;span style="color:#f92672">](&lt;/span>resp&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Response&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Either&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">ResponseException&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">String&lt;/span>&lt;span style="color:#f92672">]&lt;/span>, &lt;span style="color:#66d9ef">T&lt;/span>&lt;span style="color:#f92672">]])&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">T&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resp&lt;span style="color:#f92672">.&lt;/span>body &lt;span style="color:#66d9ef">match&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Right&lt;/span>&lt;span style="color:#f92672">(&lt;/span>value&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>pure&lt;span style="color:#f92672">(&lt;/span>value&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">case&lt;/span> &lt;span style="color:#a6e22e">Left&lt;/span>&lt;span style="color:#f92672">(&lt;/span>err&lt;span style="color:#f92672">)&lt;/span> &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#a6e22e">Async&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">F&lt;/span>&lt;span style="color:#f92672">].&lt;/span>raiseError&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#a6e22e">ExternalApiException&lt;/span>&lt;span style="color:#f92672">(&lt;/span>resp&lt;span style="color:#f92672">.&lt;/span>code&lt;span style="color:#f92672">.&lt;/span>code&lt;span style="color:#f92672">,&lt;/span> err&lt;span style="color:#f92672">.&lt;/span>getMessage&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在具体调用时针对此前定义的 &lt;code>F[_]&lt;/code> 需要显式指定副作用容器：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-scala" data-lang="scala">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">implicit&lt;/span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> ioRuntime&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">IORuntime&lt;/span> &lt;span style="color:#f92672">=&lt;/span> cats&lt;span style="color:#f92672">.&lt;/span>effect&lt;span style="color:#f92672">.&lt;/span>unsafe&lt;span style="color:#f92672">.&lt;/span>&lt;span style="color:#a6e22e">IORuntime&lt;/span>&lt;span style="color:#f92672">.&lt;/span>global
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">private&lt;/span> &lt;span style="color:#66d9ef">val&lt;/span> &lt;span style="color:#f92672">(&lt;/span>client&lt;span style="color:#f92672">,&lt;/span> releaseClient&lt;span style="color:#f92672">)&lt;/span>&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#66d9ef">APIClient&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">],&lt;/span> &lt;span style="color:#a6e22e">IO&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">Unit&lt;/span>&lt;span style="color:#f92672">])&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">val&lt;/span> resource&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">Resource&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>, &lt;span style="color:#66d9ef">MCAPIClient&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">]]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">for&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpBackend &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">HttpClientCatsBackend&lt;/span>&lt;span style="color:#f92672">.&lt;/span>resource&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">]()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authBackend &lt;span style="color:#66d9ef">&amp;lt;-&lt;/span> &lt;span style="color:#a6e22e">Resource&lt;/span>&lt;span style="color:#f92672">.&lt;/span>eval &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">TokenProvider&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>make&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">](&lt;/span>fetchToken &lt;span style="color:#66d9ef">=&lt;/span> &lt;span style="color:#a6e22e">TokenFetcher&lt;/span>&lt;span style="color:#f92672">.&lt;/span>withRetry&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">](&lt;/span>httpBackend&lt;span style="color:#f92672">,&lt;/span> oauthConfig&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">.&lt;/span>map&lt;span style="color:#f92672">(&lt;/span>tp &lt;span style="color:#66d9ef">=&amp;gt;&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">AuthBackend&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">](&lt;/span>httpBackend&lt;span style="color:#f92672">,&lt;/span> tp&lt;span style="color:#f92672">))&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span> &lt;span style="color:#66d9ef">yield&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">APIClient&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">](&lt;/span>authBackend&lt;span style="color:#f92672">,&lt;/span> &lt;span style="color:#a6e22e">Uri&lt;/span>&lt;span style="color:#f92672">.&lt;/span>parse&lt;span style="color:#f92672">(&lt;/span>baseUrl&lt;span style="color:#f92672">).&lt;/span>toOption&lt;span style="color:#f92672">.&lt;/span>get&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> resource&lt;span style="color:#f92672">.&lt;/span>allocated&lt;span style="color:#f92672">.&lt;/span>unsafeRunSync&lt;span style="color:#f92672">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/** 系统内任何地方都可以通过 `ExternalApiClientExtension(system).client` 拿到这个共享实例 */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">def&lt;/span> apiClient&lt;span style="color:#66d9ef">:&lt;/span> &lt;span style="color:#66d9ef">APIClient&lt;/span>&lt;span style="color:#f92672">[&lt;/span>&lt;span style="color:#66d9ef">IO&lt;/span>&lt;span style="color:#f92672">]&lt;/span> &lt;span style="color:#66d9ef">=&lt;/span> client
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger&lt;span style="color:#f92672">.&lt;/span>info&lt;span style="color:#f92672">(&lt;/span>&lt;span style="color:#e6db74">s&amp;#34;[ExternalAPIExtension] 外部客户端初始化完成 - baseURL: &lt;/span>&lt;span style="color:#e6db74">$baseUrl&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#f92672">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>值得一提的是这里忽略了 &lt;code>APIClient&lt;/code> 生命周期管理，如果是基于 &lt;code>Spring&lt;/code> 环境则可以利用 &lt;code>Spring&lt;/code> 框架的 &lt;code>Bean&lt;/code> 进行生命周期管理，当然在 &lt;code>Scala&lt;/code> 中选择又有所不同，具体可以根据实际情况进行讨论。&lt;/p>
&lt;h2 id="总结">总结&lt;/h2>
&lt;p>回顾整个 &lt;code>HTTP&lt;/code> 客户端的演进历程，我们不仅完成了一套代码的重构，更经历了一场从指令式工程套路到纯函数式 &lt;code>Effect&lt;/code> 系统的思想跃迁。刚开始关注如何快速实现功能但容易因缺乏资源意识而留下严重的线上隐患，后续通过引入 &lt;code>OkHttp&lt;/code>、&lt;code>单例模式&lt;/code>、&lt;code>try-with-resources&lt;/code> 与结构化异常解决了高并发下的套接字泄露与连接复用问题，达到了生产可用的标准工程质量。接着引入 &lt;code>FP&lt;/code> 理念通过 &lt;code>Vavr&lt;/code> 引入 &lt;code>Either&lt;/code> 与 &lt;code>Try&lt;/code>，将错误转化为显式的值并利用 &lt;code>flatMap&lt;/code> 与 &lt;code>recoverWith&lt;/code> 等 &lt;code>Monad&lt;/code> 算子，优雅地消除了层层嵌套的 &lt;code>if-else&lt;/code> 与受检异常，让多步依赖与降级链路变成了流畅的声明式管道。 最后通过 &lt;code>Scala + Cats Effect&lt;/code> 的纯函数式架构跨越到了纯函数式的终极形态。利用高阶类型 &lt;code>F[_]&lt;/code> 与代数数据类型我们将 HTTP 请求抽象为纯净的计算蓝图（&lt;code>IO&lt;/code>）。&lt;/p>
&lt;p>架构设计从来没有绝对的**银弹，**只有在特定语境下的权衡与选择。在实际业务中我们或许并不是每一个项目都有使用 &lt;code>Cats Effect&lt;/code> 或 &lt;code>ZIO&lt;/code> 等重型 &lt;code>FP&lt;/code> 工具的场景，乃至 &lt;code>Scala&lt;/code> 的使用也是极其罕见，但理解这四种范式的演进逻辑能让我们在面对复杂系统设计时拥有更高的视角——&lt;strong>学会如何控制副作用、如何设计严谨的类型契约，以及如何写出真正健壮、易于推导与组合的代码。&lt;/strong>&lt;/p>
&lt;h2 id="补充">补充&lt;/h2>
&lt;p>考虑到目前基于 &lt;code>Spring&lt;/code> 的微服务项目占据了绝大多数，相当多的团队都采用了基于声明式调用的 &lt;code>OpenFeign&lt;/code> 或 &lt;code>Retrofit&lt;/code> 作为服务间调用工具。这些框架虽然极大地简化了业务开发流程，但正因为其高度屏蔽了底层的网络细节与错误处理机制，容易让开发者对包括 &lt;code>TCP&lt;/code> 连接池、超时熔断以及错误码传递等在内的核心机制产生认知盲区。当遇到复杂链路故障或性能瓶颈时，缺乏对底层演进脉络理解的开发者往往无所适从。理解从最基础的 &lt;code>HttpClient&lt;/code> 到函数式 &lt;code>Effect&lt;/code> 系统的演进并不是要求在所有项目中都盲目推行 &lt;code>FP&lt;/code> 框架，而是为了让我们在面对黑盒工具时，能够保持对底层运行机制的掌控力，在架构选型与故障排查时看清事物的本质&lt;/p></content></item></channel></rss>