JDK HttpClient 阶段总结和感想

JDK HttpClient 阶段总结和感想

前几篇文章,从使用出发,比较详细地从源码上剖析了JDK11 引入的HttpClient,认识了HttpClient的结构和功能,也见证了HttpClient在进行无加密的Http1.1请求时的完整生命历程。

后续有机会,将会介绍Https及Http2请求的处理过程,或许又是一番别样的风景。

光阴似箭,日月如梭,写下本文,已在2022年的第一天,暂且总结一下JDK 新生的HttpClient的特点和存在的问题。

HttpClient

从使用的角度出发:HttpClient具有相对优秀的一面,也存在对应的不足:

  • 链式api的使用,使得调用过程更“舒服”,更像一个现代的工具类库
  • 同步和异步模式的同时支持,使其能适应更广的使用场景
  • 对Http标准的支持相对完善,是一个相对可靠的Http工具
  • 可定制性上有所缺失,比如不支持手动配置重试、连接池复用(默认无限),这些都可能成为业务上使用的顾虑
  • api的易用性还有提升的空间,比如请求头必须手动设置键值对等

得益于其内置在官方JDK中的地位,以及其还算不错的性能,可以预见的是,随着新版本JDK的逐步流行,HttpClient的未来是值得期待的,部分第三方框架和类库也可能会选择将其集成,作为远程调用的支撑手段。近水楼台先得月,正如不被看好的Edge浏览器,不也因为自己内置于windows,又凭借自身相对优良的使用体验而日渐获得众人的认可吗?

从设计的角度出发,对于日常进行业务开发的我们来说,更具有值得学习学习和深思的地方。

JDK HttpClient 阶段总结和感想

首先是关于抽象和分层。日常进行服务端开发的我,以前总以为“架构”是在服务器端才会出现的。但事实上,是软件就有架构。从一个复杂的分布式系统,到一个简单的工具类库,模块化和分层的思想总是无处不在,“高内聚,低耦合”总是软件设计的核心思想之一。一个Http请求——响应过程,外人看来只是轻而易举的事情,实际上蕴含的是复杂的逻辑。

对此,HttpClient抽象出了客户端层、交换层、连接层和管道层这些概念,让它们处理各自层面的事情。越靠近客户端层,就越偏向对生命周期的管理这些更高层次的问题;越靠近管道层,就要处理更多技术细节上的问题,包括对Socket通道和连接的控制,对字节缓冲的读写等等。“架构”在HttpClient这一组件中,不体现在不同服务间的分层和交互,而体现在组件内部各层次,或者说“各”对象间的职责分明而又相互协调。

和“抽象”这一概念相关的,还有面向对象的编程方式和设计模式的运用。我们日常进行的服务端的开发,实际上大多数只是披着面向对象的外衣,做着面向过程的事情。回想一下我们经典的MVC三层架构:Controller, Service, Dao。其中,Service名为服务,作用是实现具体的业务功能,而作为参数存在的实体类,则只是数据的载体。那么问题来了:一个对象可以对应数据库里的一个实体,也对应着现实生活中某种实体或关系的抽象。就好比一个活生生的人,既然我们认为人是有状态(外貌、年龄、阶级、信仰等等)的,又是有行为的。为什么认为业务对象是只能拥有状态和行为中的其一呢?其实,这也是领域驱动设计(DDD)所反对的,贫血模型。当然,针对这个有不少争论,本文就不再探讨了,只不过,HttpClient让我们再次见识到了面向对象编程的魅力。

回到HttpClient,我们看到了各个对象间的各司其职,看到了HttpClient的设计者通过各种模式尽可能地减少其复杂度。设计模式在HttpClient中出现得流畅自然。在响应式的发布——订阅过程中,我们多次看到,为了控制数据接收的缓冲,又或者为了解决线程竞争问题,HttpClient多次运用代理模式,增加中间层。而响应式的发布——订阅过程,本身就是经典的观察者模式的改版。

接下来要说到的,便是“响应式编程”和异步编程本身。前文多次提到,HttpClient的整个实现,都建立在CompletableFuture和Flow api上。全链路流程的异步化,无疑是JDK HttpClient强大生命力的重要来源之一。线程资源是昂贵的,如果异步的实现是每个请求一个线程(正如HttoClient同步模式一样),对网络请求这种I/O密集型场景可为是大材小用了。利用了操作系统的提供的I/O多路复用机制,HttpClient实现了I/O层面的相对高效。而类似于JAVAScript的promise/then机制的CompletableFuture的使用,则使JAVA在编程上的异步变得可以接受了,不再是以前Future的笨拙,也某种程度上摆脱了“回调地狱”。而基于数据流抽象的响应式编程模型,则很好地适应了Http请求这一数据交换的过程,被运用也是理所应当的。

不过,在之前的代码中,我们可以看到,CompletableFuture的使用,也造成了代码的可读性降低。具体而言,我们可以看到,HttpClient的代码里到处都是诸如下方所示的错误捕捉代码。

if (t != null) {
    //……
    cf.completeExectionally(……) ;//或者如下
    return MinimalFuture.failedFuture(……);
}

这也是异步编程的难点之一:如何捕捉和传递错误?我们没法再简单地用try catch捕获了。

异步编程的另一个缺陷,便是失去了有意义的堆栈。当按前后顺序编写的代码不运行在一个线程上,断点调试便不再方便。如果说框架代码还能因为需求相对明确而不至于收到太大困扰,那么业务代码呢?面对复杂多变的需求,使用异步编程还能游刃有余吗?更何况,JAVA对异步编程的支持还不完善,也并不准备变得完善。

在此之前。我们还要讨论一下关于JDK HttpClient的代码质量。作为业务开发人员的我们,在工作中见到低质量代码时,总是如此的无奈:我们并没有多少重构的机会。

必须承认的是,JDK HttpClient的代码质量总体是有保证的,这也建立在其良好的抽象分层和架构设计上。不过,我们总能看到要改进的地方:比如,抽象的单次请求——响应交换,命名居然是ExchangeImpl,不免让初读源码的人感到困惑;又比如,getOutgoing()方法,看似只是get了一个输出的东西,实际上做了读取状态切换等等重要的事情;又比如,不少类的构造方法中,实际上暗藏了连接的建立、发布——订阅过程等核心代码……这些做法真的合理吗?我持有保留态度。从好的方面讲是“封装”得深,不好的方面讲就是让人迷惑,一头雾水,可读性有待提升,还降低了代码的可维护性。在HttpClient中还看到了设计和编写者的思考和疑虑的痕迹:"can we get rid of this ?"、"should we fail if there is one?"等等,也客观上说明了软件设计的复杂性,让Oracle的工程师也禁不住挠头抓耳。

总的来说,从一个使用者和学习者的角度,我愿意给JDK HttpClient积极的评价。而它的众多妥协,在我看来也是JAVA自身的局限性导致的。

JAVA并发与异步编程

关于HttpClient就讲这么多。从一个普通业务开发人员的角度出发,我还想分享下JAVA本身。我并非JAVA”高手“,JVM的源码更是根本没看过,”精通“根本无从谈起。但透过HttpClient的实现,我不得不提下JAVA近年来缓慢的进展和不太乐观的前景。

JAVA是我第一门真正掌握以至于能熟练运用于工作的语言。它的身上还带着大量”旧时代“语言的特点。它的兴起,得益于它对网络编程的良好抽象。但时至今日,放眼望去,它的线程模型已经悄然落后。在go、python等语言实现了轻量级的协程的今天,JAVA却仍固守着1:1的线程映射模型。线程的创建相对昂贵,这对当下大量的I/O密集性应用(如网络应用)来说,显得格外不友好。此外,JAVA对异步编程的支持却仍然显得可怜,摆脱”伪异步“、”回调地狱“的CompletableFuture也在JAVA12才彻底定型。即便如此,它在python、node.js简便的async/await语法面前也相形见绌。

关于异步编程是否有未来,网络上争论不休,但至少对于回调地狱式的写法和promise-then式的写法,Oracle官方似乎给出了否定的答案。JAVA数年前就在进行的试验品:project loom,就是选择实现轻量级的用户态线程,多个用户态线程对应一个操作系统线程,用类似于普通多线程调用的方式来实现并发。如此,当创建”线程“不再昂贵,阻塞式I/O也不再可怕。

然而,project loom的前景并非如此光明,JAVA沉重的历史包袱使其频频”难产“,JAVA17这个大版本中丝毫不见其身影。在此种情况下,开发者要么接受多线程模型的落后,要么接受异步代码的难写难调。JDK 内置的HttpClient选择了后者,而我们平时的业务开发则选择了前者。

Project Loom的情况也是JAVA近年来情况的一个缩影。除了ZGC和少数的语法糖,JAVA11之后几乎难见重大更新。难怪那么多人说”你发任你发,我用JAVA8“,除了JAVA应用的企业应用领域相对保守外,没有足够的新特性支持也是重要原因吧。

写作感想:关于技术和世界

写下本文,已是2022年的元旦。在2021年的最后一个月,我完成了对JDK HttpClient的基本分析。这是我第一次写作真正意义上的技术博客(如果之前发布在CSDN的两篇不算的话)。并且,如果没有意外,这几篇文章应该是全中文网络最先从源码级别剖析这一类库的系列文章。这也是我写作最开心的事情之一:写了我认为有意义的东西,为知识的传播做了一点微不足道的贡献。

我一直思考着技术博客的写作者们初心是什么?是为了自身的学习?是为了证明自己,在职场上获得更好的竞争力?还是为了传播和分享知识?其实答案当然是兼而有之。那么,单就第三点来说,传播和分享技术知识的目的又是为了什么?是为了获得满足感,还是为了构建一个更好的世界呢?

在提交完上一篇文章的时候,已经是2021年的最后几分钟,走上大街,已是零点过后。元旦凌晨的街道有着不同以往的热闹:马路上依旧川流不息,人行道上的行人却神态各异。年轻男女们有说有笑,仿佛在畅想着他们美好的未来;路边的小贩却默不作声,抓紧最后的时间希望多几分生意。对一部分人来说,新的公历年又是新的奇妙历程的开始,而对另一部分人来说不过是周而复始般的挣扎。生长在互联网普及的时代,亲眼看见网络技术将世界变得天翻地覆,但揭开当代社会华丽的外衣,它的性质并未变化,技术在一些决定性的问题上的作为实在有限,甚至在某些方面变得更糟。

在我上中学时,我曾是个技术乐观主义者,相信随着科技的进步,人类必将过上美好的生活,如今看来却未必如此。正如互联网给人以“知识面前人人平等”的假象,实际上它对良好社会的构建实在意义有限。人们逐渐只相信他们愿意相信的,又或许一直如此。即使人们意识到了,没有行动也不会有改变。

历史的洪流滚滚向前,裹挟其中的我对这一切也只是个沉默的见证者和参与者。不过,既然历史让我暴露于这个互联网时代,又让我阴差阳错成为所谓的“技术人”,我又怎能不欣然接收呢?毕竟,希望是最宝贵的,正如它数千年前被放入盒中的模样。

这片博客写完,我也要踏上新的旅途,在技术世界中继续探索。HttpClient,希望能后会有期。

上一篇:Codeforces Round #652 (Div. 2) E - DeadLee (贪心)


下一篇:.NET SourceGenerators 根据 HTTPAPI 接口自动生成实现类