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

最近項目須要使用 Java 重度調用 HTTP API 接口,因而想着封裝一個團隊公用的 HTTP client lib. 這個庫須要支持如下特性:java

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

  2. 域名解析控制。由於調用量會比較大,所以但願在域名解析這一層作一個調用端可控的負載均衡,同時能夠對每一個服務器 IP 進行失敗率統計和健康度檢查。緩存

  3. Form/JSON 調用支持良好。服務器

  4. 支持同步和異步調用。網絡

在 Java 生態中,雖然有數不清的 HTTP client lib 組件庫,可是大致能夠分爲這三類:app

  1. JDK 自帶的 HttpURLConnection 標準庫;負載均衡

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

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

HttpURLConnection

使用 HttpURLConnection 發起 HTTP 請求最大的優勢是不須要引入額外的依賴,可是使用起來很是繁瑣,也缺少鏈接池管理、域名機械控制等特性支持。以發起一個 HTTP POST 請求爲例: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"))
  .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<String, String> params) throws Exception {
        HttpPost request = new HttpPost(url);

        // set header
        request.setHeader("X-Http-Demo", HttpComponentsDemo.class.getSimpleName());

        // set params
        if (params != null) {
            List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
            for (Map.Entry<String, String> 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<String, String> params = new HashMap<String, String>();
        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<String, String> params) throws Exception {
        FormBody.Builder builder = new FormBody.Builder(CharsetUtils.get("UTF-8"));
        if (params != null) {
            for (Map.Entry<String, String> 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<String, String> params = new HashMap<String, String>();
        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 開發。

 

擴展閱讀

幾張趣圖助你理解HTTP狀態碼

乾貨 | 完全弄懂 HTTP 緩存機制及原理

完全理解Netty,這一篇文章就夠了

完全理解SpringIOC、DI-這篇文章就夠了

揭開Java 泛型類型擦除神祕面紗

 

做者:行思錄

來源:https://liudanking.com/sitelog/java-http-client-lib-comparison/

相關文章
相關標籤/搜索