HttpClient使用不當(dāng),服務(wù)掛了!是時(shí)候系統(tǒng)學(xué)習(xí)一下了
背景
最近發(fā)生了兩件事,覺得有必要系統(tǒng)的學(xué)習(xí)一下Apache的HttpClient了。
事件一:聯(lián)調(diào)微信支付接口,用到HttpClient,花時(shí)間整理了一番。如果有一篇文章,讀一讀就可以掌握HttpClient 80%的內(nèi)容,再有可以直接用的Demo,下次再遇到是不是就可以非常容易集成了?這篇便是這篇文章的目標(biāo)之一。
事件二:上家公司同事發(fā)消息求助,說系統(tǒng)JVM溢出,找不到原因不了。查看了發(fā)來的日志文件,基本定位是HttpClient調(diào)用三方接口時(shí)內(nèi)存溢出導(dǎo)致的。
無論出于哪種原因,HTTP調(diào)用的熟練使用都是必不可少的,今天就來一起系統(tǒng)學(xué)習(xí)一下,查漏補(bǔ)缺。
HttpClient
HTTP協(xié)議的重要性不言而喻,它是現(xiàn)在Internet中使用最多,最重要的協(xié)議了。雖然JDK中已經(jīng)提供了HTTP協(xié)議的基本功能,但對于大部分應(yīng)用來說,這套API還是不夠豐富和靈活。
HttpClient是用來編程實(shí)現(xiàn)HTTP調(diào)用的一款框架,它是Apache Jakarta Common下的子項(xiàng)目,相比傳統(tǒng)JDK自帶的URLConnection,增加了易用性和靈活性。
HttpClient不僅使客戶端發(fā)送Http請求變得更加容易,而且也方便了開發(fā)人員測試接口(基于Http協(xié)議的),即提高了開發(fā)的效率,也方便提高代碼的健壯性。
目前主流的SpringCloud框架,服務(wù)與服務(wù)之間的調(diào)用也全部是基于HttpClient來實(shí)現(xiàn)的。因此,系統(tǒng)的學(xué)習(xí)一下HttpClient,還是非常有必要的。
HttpClient功能及特性
HttpClient主要提供了以下功能及特性實(shí)現(xiàn):
基于標(biāo)準(zhǔn)、純凈的java語言。實(shí)現(xiàn)了HTTP 1.0和HTTP 1.1; 以可擴(kuò)展的面向?qū)ο蟮慕Y(jié)構(gòu)實(shí)現(xiàn)了HTTP全部的方法(GET、 POST、PUT、DELETE、HEAD、OPTIONS、TRACE)等。 支持HTTPS協(xié)議。 通過HTTP代理建立透明的連接。 利用CONNECT方法通過HTTP代理建立隧道的HTTPs連接。 Basic, Digest, NTLMv1, NTLMv2, NTLM2 Session, SNPNEGO/Kerberos認(rèn)證方案。 插件式的自定義認(rèn)證方案。 便攜可靠的套接字工廠使它更容易的使用第三方解決方案。 連接管理器支持多線程應(yīng)用。支持設(shè)置最大連接數(shù),同時(shí)支持設(shè)置每個(gè)主機(jī)的最大連接數(shù),發(fā)現(xiàn)并關(guān)閉過期的連接。 自動(dòng)處理Set-Cookie中的Cookie。 插件式的自定義Cookie策略。 Request的輸出流可以避免流中內(nèi)容直接緩沖到Socket服務(wù)器。 Response的輸入流可以有效的從Socket服務(wù)器直接讀取相應(yīng)內(nèi)容。 在HTTP 1.0和HTTP1.1中利用KeepAlive保持持久連接。 直接獲取服務(wù)器發(fā)送的response code和 headers。 設(shè)置連接超時(shí)的能力。 實(shí)驗(yàn)性的支持HTTP1.1 response caching。 源代碼基于Apache License 可免費(fèi)獲取。 支持自動(dòng)(跳轉(zhuǎn))轉(zhuǎn)向;
關(guān)于以上特性,了解即可,用到時(shí)再進(jìn)行深入學(xué)習(xí)和實(shí)踐。
HttpClient使用步驟
使用HttpClient來發(fā)送請求、接收響應(yīng)通常有以下步驟:
引入依賴:項(xiàng)目中通過Maven等形式引入 HttpClient依賴類庫。創(chuàng)建 HttpClient對象。創(chuàng)建請求方法實(shí)例:GET請求創(chuàng)建 HttpGet對象,POST請求創(chuàng)建HttpPost對象,并在對象構(gòu)建時(shí)指定請求URL。設(shè)置請求參數(shù):調(diào)用 HttpGet、HttpPost共同的setParams(HetpParams params)方法來添加請求參數(shù);HttpPost也可調(diào)用setEntity(HttpEntity entity)方法來設(shè)置請求參數(shù)。發(fā)送請求:調(diào)用 HttpClient對象的execute(HttpUriRequest request)發(fā)送請求,該方法返回一個(gè)HttpResponse。獲取響應(yīng)結(jié)果:調(diào)用 HttpResponse的getAllHeaders()、getHeaders(String name)等方法獲取服務(wù)器的響應(yīng)頭;調(diào)用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務(wù)器的響應(yīng)內(nèi)容。釋放連接:無論執(zhí)行方法是否成功,都必須釋放連接。
以上便是使用HttpClient的核心步驟:引入依賴、創(chuàng)建HttpClient對象、創(chuàng)建請求實(shí)例、設(shè)置請求參數(shù)、發(fā)送請求、獲取請求結(jié)果、釋放連接。
文章剛開始提到的事件二,便是由于釋放連接不當(dāng)導(dǎo)致連接累積導(dǎo)致內(nèi)存溢出。
了解了HttpClient的使用步驟,就可以具體的代碼實(shí)現(xiàn)了。
實(shí)例代碼實(shí)戰(zhàn)
在項(xiàng)目中引入HttpClient依賴:
????org.apache.httpcomponents
????httpclient
????4.5.13
Get請求示例
先以Get請求為例,展示一下調(diào)用百度搜索Java關(guān)鍵字:
?@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,從響應(yīng)中取出實(shí)體表示的內(nèi)容并轉(zhuǎn)換成字符串
???String?string?=?EntityUtils.toString(entity,?"utf-8");
???System.out.println(string);
??}
??//?5、關(guān)閉資源
??response.close();
??httpClient.close();
?}
執(zhí)行上述代碼,HttpClient調(diào)用成功,控制臺(tái)會(huì)打印出百度返回結(jié)果的HTML信息。這個(gè)過程也遵循了上面說到的HttpClient的使用步驟。
上述代碼看似能夠正常使用,但在執(zhí)行的過程中如果出現(xiàn)異常,則會(huì)出現(xiàn)連接無法正常釋放,導(dǎo)致內(nèi)存溢出問題。
對上述代碼進(jìn)行改進(jìn):
?@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,從響應(yīng)中取出實(shí)體表示的內(nèi)容并轉(zhuǎn)換成字符串
????String?string?=?EntityUtils.toString(entity,?"utf-8");
????System.out.println(string);
???}
??}?catch?(Exception?e)?{
???//?打印堆棧信息,進(jìn)行異常情況處理;
??}?finally?{
???//?5、關(guān)閉資源
???if?(response?!=?null)?{
????try?{
?????response.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
???if?(httpClient?!=?null)?{
????try?{
?????httpClient.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
??}
?}
雖然代碼復(fù)雜了一些,但此時(shí)無論是否出現(xiàn)異常,都可以將連接進(jìn)行正常的關(guān)閉,避免內(nèi)存溢出。
在上述代碼中,其中HttpGet的參數(shù)是直接拼接到HTTP連接后面的,當(dāng)然也可以通過URI來構(gòu)建,代碼實(shí)現(xiàn)如下:
HttpGet?httpGet?=?new?HttpGet("http://www.baidu.com/s?wd=java");
//?上述實(shí)現(xiàn)等價(jià)于下面的實(shí)現(xiàn);
URI?uri?=?new?URIBuilder("http://www.baidu.com/s").setParameter("wd","java").build();
HttpGet?httpGet?=?new?HttpGet(uri);
當(dāng)然,針對資源釋放部分,還可以利用Java 8提供的try-with-resources語法糖來進(jìn)行簡化代碼。
Post請求示例
下面的實(shí)例中的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){
???//?打印堆棧信息,進(jìn)行異常情況處理;
??}?finally?{
???//?5、關(guān)閉資源
???if?(response?!=?null)?{
????try?{
?????response.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
???if?(httpClient?!=?null)?{
????try?{
?????httpClient.close();
????}?catch?(IOException?e)?{
?????e.printStackTrace();
????}
???}
??}
?}
Post請求部分與Get請求的關(guān)鍵區(qū)別在于構(gòu)建的請求對象不同,傳輸?shù)膮?shù)不再局限于URL的拼接,還可以基于Entity來進(jìn)行傳輸。我們在實(shí)踐的過程中,大多數(shù)也是將數(shù)據(jù)放在Entity中基于JSON等格式進(jìn)行傳輸。
HttpClient 超時(shí)配置
正常來說上面的代碼已經(jīng)基本滿足了業(yè)務(wù)需求,但還是有需要完善的地方,特別是針對HTTP請求超時(shí)情況的處理。
HttpClient對此提供了setConfig(RequestConfig config)方法來為請求配置超時(shí)時(shí)間等,部分核心代碼如下:
//?設(shè)置配置請求參數(shù)(沒有可忽略)
RequestConfig?requestConfig?=?RequestConfig.custom().setConnectTimeout(35000)//?連接主機(jī)服務(wù)超時(shí)時(shí)間
?.setConnectionRequestTimeout(35000)//?請求超時(shí)時(shí)間
?.setSocketTimeout(60000)//?數(shù)據(jù)讀取超時(shí)時(shí)間
?.build();
//?為httpGet實(shí)例設(shè)置配置
httpGet.setConfig(requestConfig);
關(guān)于上述配置的重要性,也是不容忽視的。否則可能會(huì)導(dǎo)致請求阻塞,影響性能等問題。
HttpClient工具類封裝
看完上述使用,是不是發(fā)現(xiàn)HttpClient的使用非常簡單、便捷?其實(shí),還可以根據(jù)具體是使用場景,進(jìn)一步進(jìn)行封裝,封裝成工具類,業(yè)務(wù)使用時(shí)直接調(diào)用即可。
關(guān)于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?{
??//設(shè)置http的狀態(tài)參數(shù)
??requestConfig?=?RequestConfig.custom()
????.setSocketTimeout(5000)
????.setConnectTimeout(5000)
????.setConnectionRequestTimeout(5000)
????.build();
??//?TODO?補(bǔ)充其他配置
?}
?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;
?}
}
上述代碼滿足了基本的功能,如果有特殊功能則可進(jìn)一步擴(kuò)展。同時(shí),static代碼塊中可進(jìn)一步完善RequestConfig的參數(shù)配置和其他配置的初始化。另外,針對異常處理部分,也看根據(jù)具體的業(yè)務(wù)場景選擇:直接拋出異常、打印日志、拋出自定義異常等方式進(jìn)行處理。
小結(jié)
本篇文章我們學(xué)習(xí)了HttpClient及其基本使用,同時(shí)以代碼的形式展示了最佳實(shí)踐、封裝、改進(jìn)以及其中會(huì)遇到的問題。掌握本篇內(nèi)容基本可以滿足80%的日常使用場景了。當(dāng)然,還有一些針對HTTPs請求、連接池配置、異步處理等特定使用,則需要讀者在實(shí)踐的過程中有針對性的自行探索了。
往期推薦
如果你覺得這篇文章不錯(cuò),那么,下篇通常會(huì)更好。添加微信好友,可備注“加群”(微信號:zhuan2quan)。
? 和花一輩子都看不清的人,
? 注定是截然不同的搬磚生涯。



