Java HTTP 組件庫(kù)選型看這篇就夠了

最近項(xiàng)目需要使用 Java 重度調(diào)用 HTTP API 接口,于是想著封裝一個(gè)團(tuán)隊(duì)公用的 HTTP client lib. 這個(gè)庫(kù)需要支持以下特性:

連接池管理,包括連接創(chuàng)建和超時(shí)、空閑連接數(shù)控制、每個(gè) host 的連接數(shù)配置等。基本上,我們想要一個(gè) go HTTP 標(biāo)準(zhǔn)庫(kù)自帶的連接池管理功能。

域名解析控制。因?yàn)檎{(diào)用量會(huì)比較大,因此希望在域名解析這一層做一個(gè)調(diào)用端可控的負(fù)載均衡,同時(shí)可以對(duì)每個(gè)服務(wù)器 IP 進(jìn)行失敗率統(tǒng)計(jì)和健康度檢查。

Form/JSON 調(diào)用支持良好。

支持同步和異步調(diào)用。

在 Java 生態(tài)中,雖然有數(shù)不清的 HTTP client lib 組件庫(kù),但是大體可以分為這三類:

JDK 自帶的?HttpURLConnection?標(biāo)準(zhǔn)庫(kù);

Apache HttpComponents HttpClient, 以及基于該庫(kù)的 wrapper, 如?Unirest.

非基于 Apache HttpComponents HttpClient, 大量重寫(xiě)應(yīng)用層代碼的 HTTP client 組件庫(kù),典型代表是?OkHttp.

HttpURLConnection

使用 HttpURLConnection 發(fā)起 HTTP 請(qǐng)求最大的優(yōu)點(diǎn)是不需要引入額外的依賴,但是使用起來(lái)非常繁瑣,也缺乏連接池管理、域名機(jī)械控制等特性支持。以發(fā)起一個(gè) HTTP POST 請(qǐng)求為例:

import java.io.BufferedReader;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.io.OutputStream;

import java.net.HttpURLConnection;

import java.net.URL;



public class HttpUrlConnectionDemo {


????public static void main(String[] args) throws Exception {

????????String urlString = "https://httpbin.org/post";

????????String bodyString = "password=e10adc3949ba59abbe56e057f20f883e&username=test3";


????????URL url = new URL(urlString);

????????HttpURLConnection conn = (HttpURLConnection) url.openConnection();

????????conn.setRequestMethod("POST");

????????conn.setDoOutput(true);


????????OutputStream os = conn.getOutputStream();

????????os.write(bodyString.getBytes("utf-8"));

????????os.flush();

????????os.close();


????????if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {

????????????InputStream is = conn.getInputStream();

????????????BufferedReader reader = new BufferedReader(new InputStreamReader(is));

????????????StringBuilder sb = new StringBuilder();

????????????String line;

????????????while ((line = reader.readLine()) != null) {

????????????????sb.append(line);

????????????}

????????????System.out.println("rsp:" + sb.toString());

????????} else {

????????????System.out.println("rsp code:" + conn.getResponseCode());

????????}

????}


}


可以看到,使用 HttpURLConnection 發(fā)起 HTTP 請(qǐng)求是比較原始(low level)的,基本上你可以理解為它就是對(duì)網(wǎng)絡(luò)棧傳輸層(HTTP 一般為 TCP,HTTP over QUIC 是 UDP)進(jìn)行了一次淺層次的封裝,操作原語(yǔ)就是在打開(kāi)的連接上面寫(xiě)請(qǐng)求 request 與讀響應(yīng) response. 而且 HttpURLConnection 無(wú)法支持 HTTP/2. 顯然,官方是知道這些問(wèn)題的,因此在 Java 9 中,官方在標(biāo)準(zhǔn)庫(kù)中引入了一個(gè) high level、支持 HTTP/2 的?HttpClient. 這個(gè)庫(kù)的接口封裝就非常主流到位了,發(fā)起一個(gè)簡(jiǎn)單的 POST 請(qǐng)求:

HttpRequest request = HttpRequest.newBuilder()

??.uri(new URI("https://postman-echo.com/post"))

??.headers("Content-Type", "text/plain;charset=UTF-8")

??.POST(HttpRequest.BodyProcessor.fromString("Sample request body"))

??.build();


封裝的最大特點(diǎn)是鏈?zhǔn)秸{(diào)用非常順滑,支持連接管理等特性。但是這個(gè)庫(kù)只能在 Java 9 及以后的版本使用,Java 9 和 Java 10 并不是 LTS 維護(hù)版本,而接下來(lái)的 Java 11 LTS 要在2018.09.25發(fā)布,應(yīng)用到線上還需要等待一段時(shí)間。因此,雖然挺喜歡這個(gè)自帶標(biāo)準(zhǔn)庫(kù)(畢竟可以不引入三方依賴),但當(dāng)前是無(wú)法在生產(chǎn)環(huán)境使用的。

Apache HttpComponents HttpClient

Apache HttpComponents HttpClient 的前身是?Apache Commons HttpClient, 但是 Apache Commons HttpClient 已經(jīng)停止開(kāi)發(fā),如果你還在使用它,請(qǐng)切換到 Apache HttpComponents HttpClient 上來(lái)。

Apache HttpComponents HttpClient 支持的特性非常豐富,完全覆蓋我們的需求,使用起來(lái)也非常順手:

<br />import java.io.ByteArrayOutputStream;

import java.io.InputStream;

import java.util.ArrayList;

import java.util.HashMap;

import java.util.List;

import java.util.Map;


import org.apache.commons.io.IOUtils;

import org.apache.http.HttpResponse;

import org.apache.http.NameValuePair;

import org.apache.http.client.entity.UrlEncodedFormEntity;

import org.apache.http.client.fluent.Request;

import org.apache.http.client.methods.CloseableHttpResponse;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.client.methods.RequestBuilder;

import org.apache.http.impl.client.CloseableHttpClient;

import org.apache.http.message.BasicNameValuePair;

import org.apache.http.impl.client.HttpClients;



public class HttpComponentsDemo {

????final static CloseableHttpClient client = HttpClients.createDefault();


????// 常規(guī)調(diào)用

????private String sendPostForm(String url, Map&lt;String, String&gt; params) throws Exception {

????????HttpPost request = new HttpPost(url);


????????// set header

????????request.setHeader("X-Http-Demo", HttpComponentsDemo.class.getSimpleName());


????????// set params

????????if (params != null) {

????????????List&lt;NameValuePair&gt; nameValuePairList = new ArrayList&lt;NameValuePair&gt;();

????????????for (Map.Entry&lt;String, String&gt; entry : params.entrySet()) {

????????????????nameValuePairList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));

????????????}

????????????UrlEncodedFormEntity bodyEntity = new UrlEncodedFormEntity(nameValuePairList, "UTF-8");

????????????//System.out.println("body:" + IOUtils.toString(bodyEntity.getContent()));

????????????request.setEntity(new UrlEncodedFormEntity(nameValuePairList));

????????}


????????// send request

????????CloseableHttpResponse response = client.execute(request);

????????// read rsp code

????????System.out.println("rsp code:" + response.getStatusLine().getStatusCode());

????????// return content

????????String ret = readResponseContent(response.getEntity().getContent());

????????response.close();

????????return ret;

????}


????// fluent 鏈?zhǔn)秸{(diào)用

????private String sendGet(String url) throws Exception {

????????return Request.Get(url)

????????????????.connectTimeout(1000)

????????????????.socketTimeout(1000)

????????????????.execute().returnContent().asString();

????}


????private String readResponseContent(InputStream inputStream) throws Exception {

????????if (inputStream == null) {

????????????return "";

????????}

????????ByteArrayOutputStream out = new ByteArrayOutputStream();

????????byte[] buf = new byte[512];

????????int len;

????????while (inputStream.available() &gt; 0) {

????????????len = inputStream.read(buf);

????????????out.write(buf, 0, len);

????????}


????????return out.toString();

????}


????public static void main(String[] args) throws Exception {

????????HttpComponentsDemo httpUrlConnectionDemo = new HttpComponentsDemo();

????????String url = "https://httpbin.org/post";

????????Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();

????????params.put("foo", "bar中文");

????????String rsp = httpUrlConnectionDemo.sendPostForm(url, params);

????????System.out.println("http post rsp:" + rsp);


????????url = "https://httpbin.org/get";

????????System.out.println("http get rsp:" + httpUrlConnectionDemo.sendGet(url));

????}

}


對(duì) Client 細(xì)致的配置和自定義支持也是非常到位的:

????????// Create a connection manager with custom configuration.

????????PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(

????????????????socketFactoryRegistry, connFactory, dnsResolver);


????????// Create socket configuration

????????SocketConfig socketConfig = SocketConfig.custom()

????????????.setTcpNoDelay(true)

????????????.build();

????????// Configure the connection manager to use socket configuration either

????????// by default or for a specific host.

????????connManager.setDefaultSocketConfig(socketConfig);

????????connManager.setSocketConfig(new HttpHost("somehost", 80), socketConfig);

????????// Validate connections after 1 sec of inactivity

????????connManager.setValidateAfterInactivity(1000);


????????// Create message constraints

????????MessageConstraints messageConstraints = MessageConstraints.custom()

????????????.setMaxHeaderCount(200)

????????????.setMaxLineLength(2000)

????????????.build();

????????// Create connection configuration

????????ConnectionConfig connectionConfig = ConnectionConfig.custom()

????????????.setMalformedInputAction(CodingErrorAction.IGNORE)

????????????.setUnmappableInputAction(CodingErrorAction.IGNORE)

????????????.setCharset(Consts.UTF_8)

????????????.setMessageConstraints(messageConstraints)

????????????.build();

????????// Configure the connection manager to use connection configuration either

????????// by default or for a specific host.

????????connManager.setDefaultConnectionConfig(connectionConfig);

????????connManager.setConnectionConfig(new HttpHost("somehost", 80), ConnectionConfig.DEFAULT);


????????// Configure total max or per route limits for persistent connections

????????// that can be kept in the pool or leased by the connection manager.

????????connManager.setMaxTotal(100);

????????connManager.setDefaultMaxPerRoute(10);

????????connManager.setMaxPerRoute(new HttpRoute(new HttpHost("somehost", 80)), 20);



完整示例請(qǐng)參考?ClientConfiguration.

基本上,在 Java 原生標(biāo)準(zhǔn)庫(kù)不給力的情況下,Apache HttpComponents HttpClient 是最佳的 HTTP Client library 選擇。但這個(gè)庫(kù)當(dāng)前還不支持 HTTP/2,支持 HTTP/2 的版本還處于 beta 階段(2018.09.23),因此并不適合用于 Android APP 中使用。

OkHttp

由于當(dāng)前 Apache HttpComponents HttpClient 版本并不支持 HTTP/2, 而 HTTP/2 對(duì)于移動(dòng)客戶端而言,無(wú)論是從握手延遲、響應(yīng)延遲,還是資源開(kāi)銷看都有相當(dāng)吸引力。因此這就給了高層次封裝且支持 HTTP/2 的 http client lib 足夠的生存空間。其中最典型的要數(shù)OkHttp.

<br /><br />import okhttp3.*;

import org.apache.http.util.CharsetUtils;

import java.util.HashMap;

import java.util.Map;



public class OkHttpDemo {

????OkHttpClient client = new OkHttpClient();


????private String sendPostForm(String url, final Map&lt;String, String&gt; params) throws Exception {

????????FormBody.Builder builder = new FormBody.Builder(CharsetUtils.get("UTF-8"));

????????if (params != null) {

????????????for (Map.Entry&lt;String, String&gt; entry: params.entrySet()) {

????????????????builder.add(entry.getKey(), entry.getValue());

????????????}

????????}

????????RequestBody requestBody = builder.build();

????????Request request = new Request.Builder().url(url).post(requestBody).build();

????????return client.newCall(request).execute().body().string();


????}


????private String sendGet(String url) throws Exception {

????????Request request = new Request.Builder().url(url).build();

????????return??client.newCall(request).execute().body().string();

????}


????public static void main(String[] args) throws Exception {

????????OkHttpDemo okHttpDemo = new OkHttpDemo();

????????String url = "https://httpbin.org/post";

????????Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();

????????params.put("foo", "bar中文");

????????String rsp = okHttpDemo.sendPostForm(url, params);

????????System.out.println("http post rsp:" + rsp);


????????url = "https://httpbin.org/get";

????????System.out.println("http get rsp:" + okHttpDemo.sendGet(url));

????}

}


OkHttp 接口設(shè)計(jì)友好,支持 HTTP/2,并且在弱網(wǎng)和無(wú)網(wǎng)環(huán)境下有自動(dòng)檢測(cè)和恢復(fù)機(jī)制,因此,是當(dāng)前 Android APP 開(kāi)發(fā)中使用最廣泛的 HTTP clilent lib 之一。

另一方面,OkHttp 提供的接口與 Java 9 中 HttpClint 接口比較類似 (嚴(yán)格講,應(yīng)該是 Java 9 借鑒了 OkHttp 等開(kāi)源庫(kù)的接口設(shè)計(jì)?),因此,對(duì)于喜歡減少依賴,鐘情于原生標(biāo)準(zhǔn)庫(kù)的開(kāi)發(fā)者來(lái)說(shuō),在 Java 11 中,從 OkHttp 切換到標(biāo)準(zhǔn)庫(kù)是相對(duì)容易的。因此,以 OkHttp 為代表的 http 庫(kù)以后的使用場(chǎng)景可能會(huì)被蠶食一部分。

小結(jié)

HttpURLConnection 封裝層次太低,并且支持特性太少,不建議在項(xiàng)目中使用。除非你的確不想引入第三方 HTTP 依賴(如減少包大小、目標(biāo)環(huán)境不提供三方庫(kù)支持等)。

Java 9 中引入的 HttpClient,封裝層次和支持特性都不錯(cuò)。但是因?yàn)?Java 版本的原因,應(yīng)用場(chǎng)景還十分有限,建議觀望一段時(shí)間再考慮在線上使用。

如果你不需要 HTTP/2特性,Apache HttpComponents HttpClient 是你的最佳選擇,比如在服務(wù)器之間的 HTTP 調(diào)用。否則,請(qǐng)使用 OkHttp, 如 Android 開(kāi)發(fā)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近項(xiàng)目需要使用 Java 重度調(diào)用 HTTP API 接口,于是想著封裝一個(gè)團(tuán)隊(duì)公用的 HTTP client ...
    傳奇內(nèi)服號(hào)閱讀 440評(píng)論 0 2
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,568評(píng)論 19 139
  • 【秋色伊人】by 文匯 秋的音符唱著想你的歌謠, 秋的紅葉浸染思念的味道, 秋的涼風(fēng)撩撥孤獨(dú)的思緒, 秋的深夜在夢(mèng)...
    仙草凝碧閱讀 683評(píng)論 1 1
  • 2017-5-20學(xué)經(jīng)匯報(bào): 一、學(xué)經(jīng)日期:2017年5月20日 農(nóng)歷四月廿五 晴 星期六 寶貝年齡:...
    b0a4ca4b06a4閱讀 373評(píng)論 0 3

友情鏈接更多精彩內(nèi)容