HttpClient使用不當,服務掛了!是時候系統(tǒng)學習一下了

背景
最近發(fā)生了兩件事,覺得有必要系統(tǒng)的學習一下Apache的HttpClient了。
事件一:聯(lián)調(diào)微信支付接口,用到HttpClient,花時間整理了一番。如果有一篇文章,讀一讀就可以掌握HttpClient 80%的內(nèi)容,再有可以直接用的Demo,下次再遇到是不是就可以非常容易集成了?這篇便是這篇文章的目標之一。
事件二:上家公司同事發(fā)消息求助,說系統(tǒng)JVM溢出,找不到原因不了。查看了發(fā)來的日志文件,基本定位是HttpClient調(diào)用三方接口時內(nèi)存溢出導致的。
無論出于哪種原因,HTTP調(diào)用的熟練使用都是必不可少的,今天就來一起系統(tǒng)學習一下,查漏補缺。
HttpClient
HTTP協(xié)議的重要性不言而喻,它是現(xiàn)在Internet中使用最多,最重要的協(xié)議了。雖然JDK中已經(jīng)提供了HTTP協(xié)議的基本功能,但對于大部分應用來說,這套API還是不夠豐富和靈活。
HttpClient是用來編程實現(xiàn)HTTP調(diào)用的一款框架,它是Apache Jakarta Common下的子項目,相比傳統(tǒng)JDK自帶的URLConnection,增加了易用性和靈活性。
HttpClient不僅使客戶端發(fā)送Http請求變得更加容易,而且也方便了開發(fā)人員測試接口(基于Http協(xié)議的),即提高了開發(fā)的效率,也方便提高代碼的健壯性。
目前主流的SpringCloud框架,服務與服務之間的調(diào)用也全部是基于HttpClient來實現(xiàn)的。因此,系統(tǒng)的學習一下HttpClient,還是非常有必要的。
HttpClient功能及特性
HttpClient主要提供了以下功能及特性實現(xiàn):
- 基于標準、純凈的java語言。實現(xiàn)了HTTP 1.0和HTTP 1.1;
- 以可擴展的面向?qū)ο蟮慕Y(jié)構(gòu)實現(xiàn)了HTTP全部的方法(GET、 POST、PUT、DELETE、HEAD、OPTIONS、TRACE)等。
- 支持HTTPS協(xié)議。
- 通過HTTP代理建立透明的連接。
- 利用CONNECT方法通過HTTP代理建立隧道的HTTPs連接。
- Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos認證方案。
- 插件式的自定義認證方案。
- 便攜可靠的套接字工廠使它更容易的使用第三方解決方案。
- 連接管理器支持多線程應用。支持設置最大連接數(shù),同時支持設置每個主機的最大連接數(shù),發(fā)現(xiàn)并關閉過期的連接。
- 自動處理Set-Cookie中的Cookie。
- 插件式的自定義Cookie策略。
- Request的輸出流可以避免流中內(nèi)容直接緩沖到Socket服務器。
- Response的輸入流可以有效的從Socket服務器直接讀取相應內(nèi)容。
- 在HTTP 1.0和HTTP1.1中利用KeepAlive保持持久連接。
- 直接獲取服務器發(fā)送的response code和 headers。
- 設置連接超時的能力。
- 實驗性的支持HTTP1.1 response caching。
- 源代碼基于Apache License 可免費獲取。
- 支持自動(跳轉(zhuǎn))轉(zhuǎn)向;
關于以上特性,了解即可,用到時再進行深入學習和實踐。
HttpClient使用步驟
使用HttpClient來發(fā)送請求、接收響應通常有以下步驟:
- 引入依賴:項目中通過Maven等形式引入
HttpClient依賴類庫。 - 創(chuàng)建
HttpClient對象。 - 創(chuàng)建請求方法實例:GET請求創(chuàng)建
HttpGet對象,POST請求創(chuàng)建HttpPost對象,并在對象構(gòu)建時指定請求URL。 - 設置請求參數(shù):調(diào)用
HttpGet、HttpPost共同的setParams(HetpParams params)方法來添加請求參數(shù);HttpPost也可調(diào)用setEntity(HttpEntity entity)方法來設置請求參數(shù)。 - 發(fā)送請求:調(diào)用
HttpClient對象的execute(HttpUriRequest request)發(fā)送請求,該方法返回一個HttpResponse。 - 獲取響應結(jié)果:調(diào)用
HttpResponse的getAllHeaders()、getHeaders(String name)等方法獲取服務器的響應頭;調(diào)用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務器的響應內(nèi)容。 - 釋放連接:無論執(zhí)行方法是否成功,都必須釋放連接。
以上便是使用HttpClient的核心步驟:引入依賴、創(chuàng)建HttpClient對象、創(chuàng)建請求實例、設置請求參數(shù)、發(fā)送請求、獲取請求結(jié)果、釋放連接。
文章剛開始提到的事件二,便是由于釋放連接不當導致連接累積導致內(nèi)存溢出。
了解了HttpClient的使用步驟,就可以具體的代碼實現(xiàn)了。
實例代碼實戰(zhàn)
在項目中引入HttpClient依賴:
????org.apache.httpcomponents
????httpclient
????4.5.13
Get請求示例
先以Get請求為例,展示一下調(diào)用百度搜索Java關鍵字:
?@Test
?public?void?testGet()?throws?IOException?{
??//1、構(gòu)建HttpClient對象
??CloseableHttpClient?httpClient?=?HttpClients.createDefault();
??//2、創(chuàng)建HttpGet,聲明get請求
??HttpGet?httpGet?=?new?HttpGet("http://www.baidu.com/s?wd=java");
??//3、發(fā)送請求
??CloseableHttpResponse?response?=?httpClient.execute(httpGet);
??//4.判斷狀態(tài)碼
??if?(response.getStatusLine().getStatusCode()?==?200)?{
???HttpEntity?entity?=?response.getEntity();
???//?使用工具類EntityUtils,從響應中取出實體表示的內(nèi)容并轉(zhuǎn)換成字符串
???String?string?=?EntityUtils.toString(entity,?"utf-8");
???System.out.println(string);
??}
??//?5、關閉資源
??response.close();
??httpClient.close();
?}
執(zhí)行上述代碼,HttpClient調(diào)用成功,控制臺會打印出百度返回結(jié)果的HTML信息。這個過程也遵循了上面說到的HttpClient的使用步驟。
上述代碼看似能夠正常使用,但在執(zhí)行的過程中如果出現(xiàn)異常,則會出現(xiàn)連接無法正常釋放,導致內(nèi)存溢出問題。
對上述代碼進行改進:
?@Test
?public?void?testGet()?{
??CloseableHttpClient?httpClient?=?null;
??CloseableHttpResponse?response?=?null;
??try?{
???//1、構(gòu)建HttpClient對象
???httpClient?=?HttpClients.createDefault();
???//2、創(chuàng)建HttpGet,聲明get請求
???HttpGet?httpGet?=?new?HttpGet("http://www.baidu.com/s?wd=java");
???//3、發(fā)送請求
???response?=?httpClient.execute(httpGet);
???//4.判斷狀態(tài)碼
???if?(response.getStatusLine().getStatusCode()?==?200)?{
????HttpEntity?entity?=?response.getEntity();
????//?使用工具類EntityUtils,從響應中取出實體表示的內(nèi)容并轉(zhuǎn)換成字符串
????String?string?=?EntityUtils.toString(entity,?"utf-8");
????System.out.println(string);
???}
??}?catch?(Exception?e)?{
???//?打印堆棧信息,進行異常情況處理;
??}?finally?{
???//?5、關閉資源
???if?(response?!=?null)?{
????try?{
?????response.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
???if?(httpClient?!=?null)?{
????try?{
?????httpClient.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
??}
?}
雖然代碼復雜了一些,但此時無論是否出現(xiàn)異常,都可以將連接進行正常的關閉,避免內(nèi)存溢出。
在上述代碼中,其中HttpGet的參數(shù)是直接拼接到HTTP連接后面的,當然也可以通過URI來構(gòu)建,代碼實現(xiàn)如下:
HttpGet?httpGet?=?new?HttpGet("http://www.baidu.com/s?wd=java");
//?上述實現(xiàn)等價于下面的實現(xiàn);
URI?uri?=?new?URIBuilder("http://www.baidu.com/s").setParameter("wd","java").build();
HttpGet?httpGet?=?new?HttpGet(uri);
當然,針對資源釋放部分,還可以利用Java 8提供的try-with-resources語法糖來進行簡化代碼。
Post請求示例
下面的實例中的Post請求相對Get請求,多了添加Header參數(shù)和Http的Entity參數(shù):
?@Test
?public?void?testPost(){
??CloseableHttpClient?httpClient?=?null;
??CloseableHttpResponse?response?=?null;
??try?{
???//1.打開瀏覽器
???httpClient?=?HttpClients.createDefault();
???//2.聲明get請求
???HttpPost?httpPost?=?new?HttpPost("https://www.oschina.net/");
???//3.網(wǎng)站為了防止惡意攻擊,在post請求中都限制了瀏覽器才能訪問
???httpPost.addHeader("User-Agent","Mozilla/5.0?(Windows?NT?10.0;?Win64;?x64)?AppleWebKit/537.36?(KHTML,?like?Gecko)?Chrome/68.0.3440.106?Safari/537.36");
???//4.判斷狀態(tài)碼
???List?parameters?=?new?ArrayList<>(0);
???parameters.add(new?BasicNameValuePair("scope",?"project"));
???parameters.add(new?BasicNameValuePair("q",?"java"));
???UrlEncodedFormEntity?formEntity?=?new?UrlEncodedFormEntity(parameters,"UTF-8");
???httpPost.setEntity(formEntity);
???//5.發(fā)送請求
???response?=?httpClient.execute(httpPost);
???if(response.getStatusLine().getStatusCode()==200){
????HttpEntity?entity?=?response.getEntity();
????String?string?=?EntityUtils.toString(entity,?"utf-8");
????System.out.println(string);
???}
??}?catch?(Exception?e){
???//?打印堆棧信息,進行異常情況處理;
??}?finally?{
???//?5、關閉資源
???if?(response?!=?null)?{
????try?{
?????response.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
???if?(httpClient?!=?null)?{
????try?{
?????httpClient.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
??}
?}
Post請求部分與Get請求的關鍵區(qū)別在于構(gòu)建的請求對象不同,傳輸?shù)膮?shù)不再局限于URL的拼接,還可以基于Entity來進行傳輸。我們在實踐的過程中,大多數(shù)也是將數(shù)據(jù)放在Entity中基于JSON等格式進行傳輸。
HttpClient 超時配置
正常來說上面的代碼已經(jīng)基本滿足了業(yè)務需求,但還是有需要完善的地方,特別是針對HTTP請求超時情況的處理。
HttpClient對此提供了setConfig(RequestConfig config)方法來為請求配置超時時間等,部分核心代碼如下:
//?設置配置請求參數(shù)(沒有可忽略)
RequestConfig?requestConfig?=?RequestConfig.custom().setConnectTimeout(35000)//?連接主機服務超時時間
?.setConnectionRequestTimeout(35000)//?請求超時時間
?.setSocketTimeout(60000)//?數(shù)據(jù)讀取超時時間
?.build();
//?為httpGet實例設置配置
httpGet.setConfig(requestConfig);
關于上述配置的重要性,也是不容忽視的。否則可能會導致請求阻塞,影響性能等問題。
HttpClient工具類封裝
看完上述使用,是不是發(fā)現(xiàn)HttpClient的使用非常簡單、便捷?其實,還可以根據(jù)具體是使用場景,進一步進行封裝,封裝成工具類,業(yè)務使用時直接調(diào)用即可。
關于HttpClientUtil的封裝有很多方式,這里提供一種封裝,僅供參考:
import?org.apache.http.HttpStatus;
import?org.apache.http.NameValuePair;
import?org.apache.http.client.config.RequestConfig;
import?org.apache.http.client.entity.UrlEncodedFormEntity;
import?org.apache.http.client.methods.CloseableHttpResponse;
import?org.apache.http.client.methods.HttpGet;
import?org.apache.http.client.methods.HttpPost;
import?org.apache.http.client.utils.URIBuilder;
import?org.apache.http.entity.StringEntity;
import?org.apache.http.impl.client.CloseableHttpClient;
import?org.apache.http.impl.client.HttpClients;
import?org.apache.http.message.BasicNameValuePair;
import?org.apache.http.util.EntityUtils;
import?java.io.IOException;
import?java.net.URI;
import?java.util.ArrayList;
import?java.util.List;
import?java.util.Map;
/**
?*?http請求客戶端
?*
?*?@author?zzs
?*/
public?class?HttpClientUtil?{
?private?static?RequestConfig?requestConfig?=?null;
?private?HttpClientUtil()?{
?}
?static?{
??//設置http的狀態(tài)參數(shù)
??requestConfig?=?RequestConfig.custom()
????.setSocketTimeout(5000)
????.setConnectTimeout(5000)
????.setConnectionRequestTimeout(5000)
????.build();
??//?TODO?補充其他配置
?}
?public?static?String?doGet(String?url,?Map?param)?{
??//?創(chuàng)建Httpclient對象
??CloseableHttpClient?httpClient?=?HttpClients.createDefault();
??String?resultString?=?"";
??CloseableHttpResponse?response?=?null;
??try?{
???//?創(chuàng)建uri
???URIBuilder?builder?=?new?URIBuilder(url);
???if?(param?!=?null)?{
????for?(String?key?:?param.keySet())?{
?????builder.addParameter(key,?param.get(key));
????}
???}
???URI?uri?=?builder.build();
???//?創(chuàng)建http?GET請求
???HttpGet?httpGet?=?new?HttpGet(uri);
???httpGet.setConfig(requestConfig);
???//?執(zhí)行請求
???response?=?httpClient.execute(httpGet);
???//?判斷返回狀態(tài)是否為200
???if?(response.getStatusLine().getStatusCode()?==?200)?{
????resultString?=?EntityUtils.toString(response.getEntity(),?"UTF-8");
???}
??}?catch?(Exception?e)?{
???//?TODO?完善異常處理
???e.printStackTrace();
??}?finally?{
???try?{
????if?(response?!=?null)?{
?????response.close();
????}
????if?(httpClient?!=?null)?{
?????httpClient.close();
????}
???}?catch?(IOException?e)?{
????e.printStackTrace();
???}
??}
??return?resultString;
?}
?public?static?String?doGet(String?url)?{
??return?doGet(url,?null);
?}
?public?static?String?doPost(String?url,?Map?param)?{
??//?創(chuàng)建Httpclient對象
??CloseableHttpClient?httpClient?=?HttpClients.createDefault();
??CloseableHttpResponse?response?=?null;
??String?resultString?=?"";
??try?{
???//?創(chuàng)建Http?Post請求
???HttpPost?httpPost?=?new?HttpPost(url);
???httpPost.setConfig(requestConfig);
???//?創(chuàng)建參數(shù)列表
???if?(param?!=?null)?{
????List?paramList?=?new?ArrayList<>();
????for?(String?key?:?param.keySet())?{
?????paramList.add(new?BasicNameValuePair(key,?(String)?param.get(key)));
????}
????//?模擬表單
????UrlEncodedFormEntity?entity?=?new?UrlEncodedFormEntity(paramList);
????httpPost.setEntity(entity);
???}
???//?執(zhí)行http請求
???response?=?httpClient.execute(httpPost);
???resultString?=?EntityUtils.toString(response.getEntity(),?"utf-8");
??}?catch?(Exception?e)?{
???//?TODO?完善異常處理
???e.printStackTrace();
??}?finally?{
???try?{
????if?(response?!=?null)?{
?????response.close();
????}
????if?(httpClient?!=?null)?{
?????httpClient.close();
????}
???}?catch?(IOException?e)?{
????e.printStackTrace();
???}
??}
??return?resultString;
?}
?public?static?String?doPost(String?url)?{
??return?doPost(url,?null);
?}
?public?static?String?doPostJson(String?url,?String?json,?String?token_header)?throws?Exception?{
??//?創(chuàng)建Httpclient對象
??CloseableHttpClient?httpClient?=?HttpClients.createDefault();
??CloseableHttpResponse?response?=?null;
??String?resultString?=?"";
??try?{
???//?創(chuàng)建Http?Post請求
???HttpPost?httpPost?=?new?HttpPost(url);
???httpPost.setConfig(requestConfig);
???//?創(chuàng)建請求內(nèi)容
???httpPost.setHeader("HTTP?Method",?"POST");
???httpPost.setHeader("Connection",?"Keep-Alive");
???httpPost.setHeader("Content-Type",?"application/json;charset=utf-8");
???httpPost.setHeader("x-authentication-token",?token_header);
???StringEntity?entity?=?new?StringEntity(json);
???entity.setContentType("application/json;charset=utf-8");
???httpPost.setEntity(entity);
???//?執(zhí)行http請求
???response?=?httpClient.execute(httpPost);
???if?(response.getStatusLine().getStatusCode()?==?HttpStatus.SC_OK)?{
????resultString?=?EntityUtils.toString(response.getEntity(),?"UTF-8");
???}
??}?catch?(Exception?e)?{
???//?TODO?完善異常處理
???e.printStackTrace();
??}?finally?{
???try?{
????if?(response?!=?null)?{
?????response.close();
????}
????if?(httpClient?!=?null)?{
?????httpClient.close();
????}
???}?catch?(IOException?e)?{
????e.printStackTrace();
???}
??}
??return?resultString;
?}
}
上述代碼滿足了基本的功能,如果有特殊功能則可進一步擴展。同時,static代碼塊中可進一步完善RequestConfig的參數(shù)配置和其他配置的初始化。另外,針對異常處理部分,也看根據(jù)具體的業(yè)務場景選擇:直接拋出異常、打印日志、拋出自定義異常等方式進行處理。
小結(jié)
本篇文章我們學習了HttpClient及其基本使用,同時以代碼的形式展示了最佳實踐、封裝、改進以及其中會遇到的問題。掌握本篇內(nèi)容基本可以滿足80%的日常使用場景了。當然,還有一些針對HTTPs請求、連接池配置、異步處理等特定使用,則需要讀者在實踐的過程中有針對性的自行探索了。

0、重磅!兩萬字長文總結(jié),梳理 Java 入門進階哪些事(推薦收藏)

