歡迎光臨
每天分享高質量文章

Java HTTP 組件庫選型看這篇就夠了

(點擊上方公眾號,可快速關註)


來源:行思錄 ,

liudanking.com/sitelog/java-http-client-lib-comparison/

最近專案需要使用 Java 重度呼叫 HTTP API 接口,於是想著封裝一個團隊公用的 HTTP client lib. 這個庫需要支持以下特性:

  1. 連接池管理,包括連接創建和超時、空閑連接數控制、每個 host 的連接數配置等。基本上,我們想要一個 go HTTP 標準庫自帶的連接吃管理功能。

  2. 域名解析控制。因為呼叫量會比較大,因此希望在域名解析這一層做一個呼叫端可控的負載均衡,同時可以對每個服務器 IP 進行失敗率統計和健康度檢查。

  3. Form/JSON 呼叫支持良好。

  4. 支持同步和異步呼叫。

在 Java 生態中,雖然有數不清的 HTTP client lib 組件庫,但是大體可以分為這三類:

  1. JDK 自帶的 HttpURLConnection 標準庫;

  2. Apache HttpComponents HttpClient, 以及基於該庫的 wrapper, 如 Unirest.

  3. 非基於 Apache HttpComponents HttpClient, 大量重寫應用層代碼的 HTTP client 組件庫,典型代表是 OkHttp.

HttpURLConnection

使用 HttpURLConnection 發起 HTTP 請求最大的優點是不需要引入額外的依賴,但是使用起來非常繁瑣,也缺乏連接池管理、域名機械控制等特性支持。以發起一個 HTTP POST 請求為例:

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 發起 HTTP 請求是比較原始(low level)的,基本上你可以理解為它就是對網絡棧傳輸層(HTTP 一般為 TCP,HTTP over QUIC 是 UDP)進行了一次淺層次的封裝,操作原語就是在打開的連接上面寫請求 request 與讀響應 response. 而且 HttpURLConnection 無法支持 HTTP/2. 顯然,官方是知道這些問題的,因此在 Java 9 中,官方在標準庫中引入了一個 high level、支持 HTTP/2 的 HttpClient. 這個庫的接口封裝就非常主流到位了,發起一個簡單的 POST 請求:

HttpRequest request = HttpRequest.newBuilder()

  .uri(new URI(“https://postman-echo.com/post”))

  .essay-headers(“Content-Type”, “text/plain;charset=UTF-8”)

  .POST(HttpRequest.BodyProcessor.fromString(“Sample request body”))

  .build();

封裝的最大特點是鏈式呼叫非常順滑,支持連接管理等特性。但是這個庫只能在 Java 9 及以後的版本使用,Java 9 和 Java 10 並不是 LTS 維護版本,而接下來的 Java 11 LTS 要在2018.09.25發佈,應用到線上還需要等待一段時間。因此,雖然挺喜歡這個自帶標準庫(畢竟可以不引入三方依賴),但當前是無法在生產環境使用的。

Apache HttpComponents HttpClient

Apache HttpComponents HttpClient 的前身是 Apache Commons HttpClient, 但是 Apache Commons HttpClient 已經停止開發,如果你還在使用它,請切換到 Apache HttpComponents HttpClient 上來。

Apache HttpComponents HttpClient 支持的特性非常豐富,完全改寫我們的需求,使用起來也非常順手:

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();

 

    // 常規呼叫

    private String sendPostForm(String url, Map params) throws Exception {

        HttpPost request = new HttpPost(url);

 

        // set essay-header

        request.setHeader(“X-Http-Demo”, HttpComponentsDemo.class.getSimpleName());

 

        // set params

        if (params != null) {

            List nameValuePairList = new ArrayList();

            for (Map.Entry 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 鏈式呼叫

    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() > 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 params = new HashMap();

        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));

    }

}

對 Client 細緻的配置和自定義支持也是非常到位的:

// 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);

完整示例請參考 ClientConfiguration.

https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientConfiguration.java

基本上,在 Java 原生標準庫不給力的情況下,Apache HttpComponents HttpClient 是最佳的 HTTP Client library 選擇。但這個庫當前還不支持 HTTP/2,支持 HTTP/2 的版本還處於 beta 階段(2018.09.23),因此並不適合用於 Android APP 中使用。

OkHttp

由於當前 Apache HttpComponents HttpClient 版本並不支持 HTTP/2, 而 HTTP/2 對於移動客戶端而言,無論是從握手延遲、響應延遲,還是資源開銷看都有相當吸引力。因此這就給了高層次封裝且支持 HTTP/2 的 http client lib 足夠的生存空間。其中最典型的要數OkHttp.

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 params) throws Exception {

        FormBody.Builder builder = new FormBody.Builder(CharsetUtils.get(“UTF-8”));

        if (params != null) {

            for (Map.Entry 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 params = new HashMap();

        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 接口設計友好,支持 HTTP/2,並且在弱網和無網環境下有自動檢測和恢復機制,因此,是當前 Android APP 開發中使用最廣泛的 HTTP clilent lib 之一。

另一方面,OkHttp 提供的接口與 Java 9 中 HttpClint 接口比較類似 (嚴格講,應該是 Java 9 借鑒了 OkHttp 等開源庫的接口設計?),因此,對於喜歡減少依賴,鐘情於原生標準庫的開發者來說,在 Java 11 中,從 OkHttp 切換到標準庫是相對容易的。因此,以 OkHttp 為代表的 http 庫以後的使用場景可能會被蠶食一部分。

小結

  • HttpURLConnection 封裝層次太低,並且支持特性太少,不建議在專案中使用。除非你的確不想引入第三方 HTTP 依賴(如減少包大小、標的環境不提供三方庫支持等)。

  • Java 9 中引入的 HttpClient,封裝層次和支持特性都不錯。但是因為 Java 版本的原因,應用場景還十分有限,建議觀望一段時間再考慮在線上使用。

  • 如果你不需要 HTTP/2特性,Apache HttpComponents HttpClient 是你的最佳選擇,比如在服務器之間的 HTTP 呼叫。否則,請使用 OkHttp, 如 Android 開發。

擴展閱讀

  • Recommendations for Java http client libraries

    Recommendations for Java http client libraries? from java

  • Google HTTP for java

    https://developers.google.com/api-client-library/java/google-http-java-client/setup

  • Apache HttpComponents HttpClient Examples

    https://hc.apache.org/httpcomponents-client-ga/examples.html

【關於投稿】


如果大家有原創好文投稿,請直接給公號發送留言。


① 留言格式:
【投稿】+《 文章標題》+ 文章鏈接

② 示例:
【投稿】《不要自稱是程式員,我十多年的 IT 職場總結》:http://blog.jobbole.com/94148/

③ 最後請附上您的個人簡介哈~



看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

赞(0)

分享創造快樂