无聊的时候是否想过response = httpClient.execute(httpGet)
对你干了啥?
题图:from Zoommy
1. 引言
HttpClient作为Http工具在Java开发中十分流行。
只要调用response = httpClient.execute(httpGet)
,开发者便可以拿到Http响应。看起来十分简单。但是,你是否好奇过这段代码究竟做了什么?
本文就通过分析HttpClient源码,来理顺一遍httpClient.execute(httpGet)
代码段的主要执行逻辑。
2. HttpClient
想知道httpClient.execute(httpGet)
做了什么,首先要知道其调用发起者httpclient
是什么。
定义:HttpClient接口定义了最基本的用于Httpclient请求的约定。具体的约束、身份验证、重定向等实现细节由不同的实现类来完成。
HttpClient中所有的请求都是由该接口的实现类来执行完成的,常见代码:httpClient.execute(httpGet)
。
2.1 以InternalHttpClient为例
InternalHttpClient
类中实现了doExecute
方法,这个方法就是执行Http请求的核心。
分析源码就不难发现,该方法就干了两件事,包装、调用execChain.execute
。说白了,真正发起请求的不是HttpClient,而是这个execChain
——ClientExecChain
。
|
|
3. ClientExecChain
execChain.execute
看起来只是一个方法调用,实际上他是一串调用链。
定义:ClientExecChain接口定义了一个Http请求执行链上的节点,每个节点既可以是另一个节点的装饰器也可以是最终节点。该接口只包含一个待实现方法(execute
)。此方法的实现,既可以是将请求传递给下一个节点执行,也可以是真实的向服务端传输请求。
如图。
- 如果ClientExecChain对象中包含另一个ClientExecChain对象
那么他就是执行链上的一个装饰节点,核心还是调用内部ClientExecChain对象的execute
,只不过在执行前后添加一些装饰逻辑。 - 如果ClientExecChain对象中包含一个HttpRequestExecutor对象
那么他就是执行链的最终节点,直接调用HttpRequestExecutor对象的execute
,也就是真正向服务端发送Http请求。
由以上分析,可以发现,最终节点只可能是MinimalClientExec、MainClientExec两者之一。
下面分别就装饰节点和最终节点进行分析。
3.1 装饰节点,以ProtocolExec为例
ProtocolExec
类的代码十分干脆,直接实现execute
方法,做一些Http协议相关的装饰逻辑。如上节所说,ProtocolExec
是执行链上的一个装饰节点。阅读源码也确实发现,他只做了三件事。
- 解析request中的uri、target等属性
- 在调用下一节点的
execute
前后添加装饰逻辑(拦截器方式) - 调用下一个节点的
execute
方法
|
|
3.2 最终节点,以MainClientExec为例
通过执行链上节点间的逐级调用,最终来到了执行链的终点,Http请求的真正发起者。由于其execute
方法过长,这里选择性省略。其主要内容包含三部分。
- 包含大量身份认证、连接校验、设置超时、设置存活时间代码
- 创建socket链路(
establishRoute
) - 调用HttpRequestExecutor对象的
execute
方法
|
|
4. 发起Http请求,HttpRequestExecutor
类如其名,HttpRequest请求执行者。其execute
方法很简单,直接调用HttpClientConnection
类的方法实现发送request接收response,而后将response返回。至此,Http请求的所有组装过程结束,请求发出,接收响应。
|
|
5. 总结
httpClient.execute(httpGet)
短短一行代码,实际内部包含了大量逻辑,请求执行链上的每一个节点都走过一遍、所有附加逻辑都组装到一起,Http请求才最终发出。
只有通过了解httpClient.execute(httpGet)
的内部逻辑,我们才能了解HttpClient的设计理念,我们才能了解到之前没有注意到的细节。
希望这篇分析,可以抛砖引玉,引发大家深究的兴趣。也借由这篇分析为大家理清代码的脉络,方便追踪、分析。
6. 后续
- 如果继续往下跟下去,具体实现涉及流操作、原始数据的解析等,最后跟到native方法,改由jvm实现。
- 如果想继续跟下去,可以使用一些小技巧,比如将socketTimeout设置为1,迫使其产生超时异常,通过异常栈即可找到调用路径。