Java HTTP 元件庫選型看這篇就夠了
Java HTTP 元件庫選型看這篇就夠了
最近專案需要使用 Java 重度呼叫 HTTP API 介面,於是想著封裝一個團隊公用的 HTTP client lib. 這個庫需要支援以下特性:
- 連線池管理,包括連線建立和超時、空閒連線數控制、每個 host 的連線數配置等。基本上,我們想要一個 go HTTP 標準庫自帶的連線吃管理功能。
- 域名解析控制。因為呼叫量會比較大,因此希望在域名解析這一層做一個呼叫端可控的負載均衡,同時可以對每個伺服器 IP 進行失敗率統計和健康度檢查。
- Form/JSON 呼叫支援良好。
- 支援同步和非同步呼叫。
在 Java 生態中,雖然有數不清的 HTTP client lib 元件庫,但是大體可以分為這三類:
-
JDK 自帶的
HttpURLConnection
標準庫; - ofollow,noindex" target="_blank">Apache HttpComponents HttpClient , 以及基於該庫的 wrapper, 如Unirest.
- 非基於 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")) .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 支援的特性非常豐富,完全覆蓋我們的需求,使用起來也非常順手:
<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(); // 常規呼叫 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 .
基本上,在 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 .
<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<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 開發。
擴充套件閱讀
- Recommendations for Java http client libraries
- Google HTTP for java
- Apache HttpComponents HttpClient Examples
–EOF–