最近項(xiàng)目需要使用 Java 重度調(diào)用 HTTP API 接口,于是想著封裝一個(gè)團(tuán)隊(duì)公用的 HTTP client lib. 這個(gè)庫需要支持以下特性:
連接池管理厨诸,包括連接創(chuàng)建和超時(shí)蚌吸、空閑連接數(shù)控制妓布、每個(gè) host 的連接數(shù)配置等酌伊。基本上浴鸿,我們想要一個(gè) go HTTP 標(biāo)準(zhǔn)庫自帶的連接吃管理功能。
域名解析控制弦追。因?yàn)檎{(diào)用量會比較大岳链,因此希望在域名解析這一層做一個(gè)調(diào)用端可控的負(fù)載均衡,同時(shí)可以對每個(gè)服務(wù)器 IP 進(jìn)行失敗率統(tǒng)計(jì)和健康度檢查劲件。
Form/JSON 調(diào)用支持良好掸哑。
支持同步和異步調(diào)用约急。
在 Java 生態(tài)中,雖然有數(shù)不清的 HTTP client lib 組件庫苗分,但是大體可以分為這三類:
JDK 自帶的HttpURLConnection標(biāo)準(zhǔn)庫厌蔽;
Apache HttpComponents HttpClient, 以及基于該庫的 wrapper, 如Unirest.
非基于 Apache HttpComponents HttpClient, 大量重寫應(yīng)用層代碼的 HTTP client 組件庫摔癣,典型代表是OkHttp.
HttpURLConnection
使用 HttpURLConnection 發(fā)起 HTTP 請求最大的優(yōu)點(diǎn)是不需要引入額外的依賴奴饮,但是使用起來非常繁瑣,也缺乏連接池管理择浊、域名機(jī)械控制等特性支持戴卜。以發(fā)起一個(gè) HTTP POST 請求為例:
importjava.io.BufferedReader;
importjava.io.InputStream;
importjava.io.InputStreamReader;
importjava.io.OutputStream;
importjava.net.HttpURLConnection;
importjava.net.URL;
publicclassHttpUrlConnectionDemo {
????publicstaticvoidmain(String[] args) throwsException {
????????String urlString = "https://httpbin.org/post";
????????String bodyString = "password=e10adc3949ba59abbe56e057f20f883e&username=test3";
????????URL url = newURL(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 = newBufferedReader(newInputStreamReader(is));
????????????StringBuilder sb = newStringBuilder();
????????????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 請求是比較原始(low level)的琢岩,基本上你可以理解為它就是對網(wǎng)絡(luò)棧傳輸層(HTTP 一般為 TCP投剥,HTTP over QUIC 是 UDP)進(jìn)行了一次淺層次的封裝,操作原語就是在打開的連接上面寫請求 request 與讀響應(yīng) response. 而且 HttpURLConnection 無法支持 HTTP/2. 顯然,官方是知道這些問題的担孔,因此在 Java 9 中薇缅,官方在標(biāo)準(zhǔn)庫中引入了一個(gè) high level、支持 HTTP/2 的?HttpClient. 這個(gè)庫的接口封裝就非常主流到位了攒磨,發(fā)起一個(gè)簡單的 POST 請求:
1
HttpRequest request = HttpRequest.newBuilder()
??.uri(newURI("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è)庫只能在 Java 9 及以后的版本使用娩缰,Java 9 和 Java 10 并不是 LTS 維護(hù)版本灸撰,而接下來的 Java 11 LTS 要在2018.09.25發(fā)布,應(yīng)用到線上還需要等待一段時(shí)間拼坎。因此浮毯,雖然挺喜歡這個(gè)自帶標(biāo)準(zhǔn)庫(畢竟可以不引入三方依賴),但當(dāng)前是無法在生產(chǎn)環(huán)境使用的泰鸡。
Apache HttpComponents HttpClient
Apache HttpComponents HttpClient 的前身是Apache Commons HttpClient, 但是 Apache Commons HttpClient 已經(jīng)停止開發(fā)债蓝,如果你還在使用它,請切換到 Apache HttpComponents HttpClient 上來盛龄。
Apache HttpComponents HttpClient 支持的特性非常豐富饰迹,完全覆蓋我們的需求,使用起來也非常順手:
importjava.io.ByteArrayOutputStream;
importjava.io.InputStream;
importjava.util.ArrayList;
importjava.util.HashMap;
importjava.util.List;
importjava.util.Map;
importorg.apache.commons.io.IOUtils;
importorg.apache.http.HttpResponse;
importorg.apache.http.NameValuePair;
importorg.apache.http.client.entity.UrlEncodedFormEntity;
importorg.apache.http.client.fluent.Request;
importorg.apache.http.client.methods.CloseableHttpResponse;
importorg.apache.http.client.methods.HttpPost;
importorg.apache.http.client.methods.RequestBuilder;
importorg.apache.http.impl.client.CloseableHttpClient;
importorg.apache.http.message.BasicNameValuePair;
importorg.apache.http.impl.client.HttpClients;
publicclassHttpComponentsDemo {
????finalstaticCloseableHttpClient client = HttpClients.createDefault();
????// 常規(guī)調(diào)用
????privateString sendPostForm(String url, Map<String, String> params) throwsException {
????????HttpPost request = newHttpPost(url);
????????// set header
????????request.setHeader("X-Http-Demo", HttpComponentsDemo.class.getSimpleName());
????????// set params
????????if(params != null) {
????????????List<NameValuePair> nameValuePairList = newArrayList<NameValuePair>();
????????????for(Map.Entry<String, String> entry : params.entrySet()) {
????????????????nameValuePairList.add(newBasicNameValuePair(entry.getKey(), entry.getValue()));
????????????}
????????????UrlEncodedFormEntity bodyEntity = newUrlEncodedFormEntity(nameValuePairList, "UTF-8");
????????????//System.out.println("body:" + IOUtils.toString(bodyEntity.getContent()));
????????????request.setEntity(newUrlEncodedFormEntity(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();
????????returnret;
????}
????// fluent 鏈?zhǔn)秸{(diào)用
????privateString sendGet(String url) throwsException {
????????returnRequest.Get(url)
????????????????.connectTimeout(1000)
????????????????.socketTimeout(1000)
????????????????.execute().returnContent().asString();
????}
????privateString readResponseContent(InputStream inputStream) throwsException {
????????if(inputStream == null) {
????????????return"";
????????}
????????ByteArrayOutputStream out = newByteArrayOutputStream();
????????byte[] buf = newbyte[512];
????????intlen;
????????while(inputStream.available() > 0) {
????????????len = inputStream.read(buf);
????????????out.write(buf, 0, len);
????????}
????????returnout.toString();
????}
????publicstaticvoidmain(String[] args) throwsException {
????????HttpComponentsDemo httpUrlConnectionDemo = newHttpComponentsDemo();
????????String url = "https://httpbin.org/post";
????????Map<String, String> params = newHashMap<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 細(xì)致的配置和自定義支持也是非常到位的:
// Create a connection manager with custom configuration.
PoolingHttpClientConnectionManager connManager = newPoolingHttpClientConnectionManager(
????????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(newHttpHost("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(newHttpHost("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(newHttpRoute(newHttpHost("somehost", 80)), 20);
完整示例請參考?ClientConfiguration.
基本上余舶,在 Java 原生標(biāo)準(zhǔn)庫不給力的情況下啊鸭,Apache HttpComponents HttpClient 是最佳的 HTTP Client library 選擇。但這個(gè)庫當(dāng)前還不支持 HTTP/2匿值,支持 HTTP/2 的版本還處于 beta 階段(2018.09.23)赠制,因此并不適合用于 Android APP 中使用。
OkHttp
由于當(dāng)前 Apache HttpComponents HttpClient 版本并不支持 HTTP/2, 而 HTTP/2 對于移動客戶端而言挟憔,無論是從握手延遲钟些、響應(yīng)延遲烟号,還是資源開銷看都有相當(dāng)吸引力。因此這就給了高層次封裝且支持 HTTP/2 的 http client lib 足夠的生存空間政恍。其中最典型的要數(shù)OkHttp.
importokhttp3.*;
importorg.apache.http.util.CharsetUtils;
importjava.util.HashMap;
importjava.util.Map;
publicclassOkHttpDemo {
????OkHttpClient client = newOkHttpClient();
????privateString sendPostForm(String url, finalMap<String, String> params) throwsException {
????????FormBody.Builder builder = newFormBody.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 = newRequest.Builder().url(url).post(requestBody).build();
????????returnclient.newCall(request).execute().body().string();
????}
????privateString sendGet(String url) throwsException {
????????Request request = newRequest.Builder().url(url).build();
????????returnclient.newCall(request).execute().body().string();
????}
????publicstaticvoidmain(String[] args) throwsException {
????????OkHttpDemo okHttpDemo = newOkHttpDemo();
????????String url = "https://httpbin.org/post";
????????Map<String, String> params = newHashMap<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 接口設(shè)計(jì)友好汪拥,支持 HTTP/2,并且在弱網(wǎng)和無網(wǎng)環(huán)境下有自動檢測和恢復(fù)機(jī)制抚垃,因此喷楣,是當(dāng)前 Android APP 開發(fā)中使用最廣泛的 HTTP clilent lib 之一。
另一方面鹤树,OkHttp 提供的接口與 Java 9 中 HttpClint 接口比較類似 (嚴(yán)格講铣焊,應(yīng)該是 Java 9 借鑒了 OkHttp 等開源庫的接口設(shè)計(jì)?)罕伯,因此曲伊,對于喜歡減少依賴,鐘情于原生標(biāo)準(zhǔn)庫的開發(fā)者來說追他,在 Java 11 中坟募,從 OkHttp 切換到標(biāo)準(zhǔn)庫是相對容易的。因此邑狸,以 OkHttp 為代表的 http 庫以后的使用場景可能會被蠶食一部分懈糯。
小結(jié)
HttpURLConnection 封裝層次太低,并且支持特性太少单雾,不建議在項(xiàng)目中使用赚哗。除非你的確不想引入第三方 HTTP 依賴(如減少包大小、目標(biāo)環(huán)境不提供三方庫支持等)硅堆。
Java 9 中引入的 HttpClient屿储,封裝層次和支持特性都不錯。但是因?yàn)?Java 版本的原因渐逃,應(yīng)用場景還十分有限够掠,建議觀望一段時(shí)間再考慮在線上使用。
如果你不需要 HTTP/2特性茄菊,Apache HttpComponents HttpClient 是你的最佳選擇疯潭,比如在服務(wù)器之間的 HTTP 調(diào)用。否則买羞,請使用 OkHttp, 如 Android 開發(fā)袁勺。
擴(kuò)展閱讀