1.1 執(zhí)行請(qǐng)求
HttpClient最基本的功能是執(zhí)行HTTP方法。一個(gè)HTTP方法的執(zhí)行涉及到一個(gè)或多個(gè)HTTP請(qǐng)求和響應(yīng)的交換,這通常是在HttpClient內(nèi)部處理的微姊。用戶需要提供一個(gè)請(qǐng)求對(duì)象牛隅,HttpClient負(fù)責(zé)傳輸這個(gè)請(qǐng)求到目標(biāo)服務(wù)器并返回相對(duì)應(yīng)的響應(yīng)的對(duì)象,如果執(zhí)行不成功船逮,則拋出異常皿曲。
HttpClient API的入口就是HttpClient
接口唱逢。
以下是最簡(jiǎn)單的形式的執(zhí)行請(qǐng)求的例子:
public void chapter1_1() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
// handle response
} finally {
response.close();
}
}
1.1.1 HTTP請(qǐng)求
所有的HTTP請(qǐng)求都有一個(gè)請(qǐng)求頭,其中包含方法名屋休、請(qǐng)求URI和HTTP協(xié)議版本號(hào)坞古。
HttpClient
支持所有HTTP/1.1
規(guī)格中定義的HTTP方法,包括:GET
,HEAD
, POST
, PUT
, DELETE
, TRACE
和 OPTIONS
劫樟。每種方法都有與之對(duì)應(yīng)的類(lèi):HttpGet
, HttpHead
, HttpPost
, HttpPut
, HttpDelete
, HttpTrace
和 HttpOptions
痪枫。
請(qǐng)求URI是請(qǐng)求資源的統(tǒng)一資源描述符(Uniform Resource Identifier)织堂。HTTP請(qǐng)求URI包含協(xié)議(protocol schema)、主機(jī)名(host name)奶陈、可選的端口(optional port)易阳、資源路徑(resource path)、可選的查詢(optional query)和可選的分塊(optional fragment)吃粒。
HttpGet httpGet = new HttpGet(
"http://www.google.com/search?h1=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient
提供 URIBuilder
工具類(lèi)來(lái)簡(jiǎn)化創(chuàng)建和修改請(qǐng)求URI:
URI uri = new URIBuilder()
.setScheme("http")
.setHost("www.google.com")
.setPath("/search")
.setParameter("h1", "en")
.setParameter("q", "httpclient")
.setParameter("btnG", "Google+Search")
.setParameter("aq", "f")
.setParameter("oq", "")
.build();
HttpGet httpGet1 = new HttpGet(uri);
System.out.println(httpGet1.getURI());
1.1.2 HTTP響應(yīng)
HTTP響應(yīng)是服務(wù)器接收和處理完請(qǐng)求報(bào)文后所返回的報(bào)文潦俺。報(bào)文的第一行由協(xié)議版本、狀態(tài)碼和及其描述組成徐勃。
public void chapter1_1_2() throws Exception {
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
System.out.println(response.getProtocolVersion());
System.out.println(response.getStatusLine().getStatusCode());
System.out.println(response.getStatusLine().getReasonPhrase());
System.out.println(response.getStatusLine().toString());
}
輸出>
HTTP/1.1
200
OK
HTTP/1.1 200 OK
1.1.3 報(bào)文頭部
HTTP報(bào)文包含一些用于描述報(bào)文的頭部信息事示,如內(nèi)容長(zhǎng)度、內(nèi)容類(lèi)型等等 僻肖。HttpClient
提供方法去獲取很魂、添加、移除和列舉這些頭部信息檐涝。
public void chapter1_1_3() {
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\"; domain=\"localhost\"");
Header h1 = response.getFirstHeader("Set-Cookie");
System.out.println(h1);
Header h2 = response.getLastHeader("Set-Cookie");
System.out.println(h2);
Header[] hs = response.getHeaders("Set-Cookie");
System.out.println(hs.length);
}
輸出>
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/"; domain="localhost"
2
獲取所有給定類(lèi)型頭部信息最有效方式是使用HeaderIterator
接口。
public void chapter1_1_3_2() {
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\"; domain=\"localhost\"");
BasicHeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator("Set-Cookie"));
while (it.hasNext()) {
HeaderElement elem = it.nextElement();
System.out.println(elem.getName() + " = " + elem.getValue());
NameValuePair[] params = elem.getParameters();
for (NameValuePair param : params) {
System.out.println(" " + param);
}
}
}
輸出>
c1 = a
path=/
domain=localhost
c2 = b
path=/
domain=localhost
1.1.4 HTTP實(shí)體
HTTP報(bào)文可以攜帶與請(qǐng)求或響應(yīng)相關(guān)聯(lián)的內(nèi)容實(shí)體法挨。因?yàn)閷?shí)體是可選的谁榜,并不是所有的請(qǐng)求和響應(yīng)中都包含實(shí)體。使用實(shí)體的請(qǐng)求被稱為包含實(shí)體的請(qǐng)求(entity enclosing request)
凡纳。HTTP規(guī)格中定義了2種包含實(shí)體的請(qǐng)求的方法:POST
和 PUT
窃植。
響應(yīng)通常會(huì)包含一個(gè)內(nèi)容實(shí)體。但也是例外荐糜,如HEAD
方法的響應(yīng)和 204 No Content
, 304 Not Modified
, 205 Reset Content
響應(yīng)巷怜。
HttpClient
區(qū)分三種實(shí)體類(lèi)型,取決于它們內(nèi)容的來(lái)源:
-
streamed: 內(nèi)容是從流(stream)中獲得暴氏,或聯(lián)機(jī)生成的延塑。這里包含從HTTP響應(yīng)中的實(shí)體。
流式實(shí)體(streamed entities)
通常是不能重復(fù)的答渔。 -
self-contained: 內(nèi)容是在內(nèi)存里关带。
自包含實(shí)體(self-contained entities)
通常是可重復(fù)的。這種類(lèi)型的實(shí)體最多用于包含實(shí)體的請(qǐng)求(entity enclosing request)
沼撕。 - wrapping: 內(nèi)容從另一實(shí)體獲得宋雏。
當(dāng)從HTTP響應(yīng)中獲取數(shù)據(jù)流時(shí),這些區(qū)分對(duì)于連接管理來(lái)說(shuō)是重要的务豺。對(duì)于應(yīng)用創(chuàng)建的請(qǐng)求實(shí)體磨总,且僅使用HttpClient
來(lái)發(fā)送,streamed
還是 self-contained
的區(qū)別就不重要了笼沥。這種情況下蚪燕,建議把不可重復(fù)的實(shí)體歸為streamed
類(lèi)型娶牌,可重復(fù)的為self-contained
類(lèi)型。
1.1.4.1 可重復(fù)實(shí)體
可重復(fù)實(shí)體是指它的內(nèi)容能夠被重復(fù)讀取邻薯。只有自包含實(shí)體(self-contained entities)
才是可重復(fù)的(如ByteArrayEntity
或StringEntity
)裙戏。
1.1.4.2 使用HTTP實(shí)體
因?yàn)閷?shí)體可表示二進(jìn)制和字符內(nèi)容,所以它是支持字符編碼的厕诡。
實(shí)體被創(chuàng)建的時(shí)機(jī)有 a) 執(zhí)行包含內(nèi)容的請(qǐng)求累榜; b) 請(qǐng)求成功后,響應(yīng)體使用實(shí)體將結(jié)果返回灵嫌。
為了從輸入報(bào)文的實(shí)體中讀取內(nèi)容壹罚,我們可以通過(guò)HttpEntity#getContent()
方法獲取輸入流java.io.InputStream
, 或者我們可以通過(guò)HttpEntity#writeTo(OutpusStream)
方法將其寫(xiě)到另一個(gè)給定的輸出流中。
當(dāng)從響應(yīng)報(bào)文中接收到實(shí)體后寿羞,HttpEntity#getContentType
和HttpEntity#getContentLength
方法可以用來(lái)讀取通用的元數(shù)據(jù)猖凛,如Content-Type
和Content-Length
頭部信息(如果有的話)。Content-Type
頭部對(duì)于文本類(lèi)型的多媒體類(lèi)型(如text/plain
或text/html
)來(lái)說(shuō)可能包含字符編碼的信息绪穆,HttpEntity#getContentEncoding()
方法可能用來(lái)讀取該信息辨泳。如果頭部不可用的話,HttpEntity#getContentLength
返回-1玖院,HttpEntity#getContentType
返回NULL菠红。如果Content-Type
可用的話,Header
對(duì)象將會(huì)被返回难菌。
當(dāng)給輸出報(bào)文創(chuàng)建實(shí)體试溯,元數(shù)據(jù)必須使用實(shí)體的創(chuàng)建者方法來(lái)創(chuàng)建:
public void chapter1_1_4() throws IOException {
StringEntity myEntity = new StringEntity("important message",
ContentType.create("text/plain", "UTF-8"));
System.out.println(myEntity.getContentType());
System.out.println(myEntity.getContentLength());
System.out.println(EntityUtils.toString(myEntity));
System.out.println(EntityUtils.toByteArray(myEntity).length);
}
輸出>
Content-Type: text/plain; charset=UTF-8
17
important message
17
1.1.5 確保釋放底層資源
為了確保正確地的釋放系統(tǒng)資源,我們必須關(guān)閉實(shí)體關(guān)聯(lián)的內(nèi)容流(stream)或者響應(yīng)(response)本身郊酒。
public void chapter1_1_5() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
InputStream inputStream = entity.getContent();
try {
// do something with inputStream
} finally {
inputStream.close();
}
}
} finally {
response.close();
}
}
關(guān)閉內(nèi)容流和關(guān)閉響應(yīng)的區(qū)別在于遇绞,前者通過(guò)消費(fèi)實(shí)體內(nèi)容來(lái)試圖保持底層連接,后者會(huì)立即關(guān)閉并且丟棄該連接燎窘。
請(qǐng)注意HttpEntity#writeTo(OUtputStream)
方法也需要確保正確釋放系統(tǒng)資源摹闽。如果這個(gè)方法通過(guò)調(diào)用HttpEntity#getContent
方法來(lái)獲取的java.io.InputStream
實(shí)例,這也需要在一個(gè)finally子句中將其關(guān)閉荠耽。
我們也可以使用EntityUtils#consume(HttpEntity)
方法來(lái)確認(rèn)實(shí)體內(nèi)容被完全消費(fèi)并且底層流被關(guān)閉钩骇。
有一種情況,如果僅需要讀取響應(yīng)中的一部分內(nèi)容铝量,并且報(bào)文剩余內(nèi)容的性能代價(jià)和保持連接的代價(jià)太高的話倘屹,我們可以通過(guò)關(guān)閉響應(yīng)來(lái)結(jié)束內(nèi)容流。
public void chapter1_1_5_1() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
InputStream inputStream = entity.getContent();
int byteOne = inputStream.read();
int byteTwo = inputStream.read();
// Do not need the rest
}
} finally {
response.close();
}
}
連接將不會(huì)被重用慢叨,而且被該連接持有的所有資源將會(huì)被正確地釋放纽匙。
1.1.6 消費(fèi)實(shí)體內(nèi)容
消費(fèi)一個(gè)實(shí)體的內(nèi)容推薦的方法是使用HttpEntity#getContent()
或HttpEntity#writeTo(OutpusStream)
方法。HttpClient也包含EntityUtils
類(lèi)拍谐,其他包含一些靜態(tài)方法可以理容易地讀取實(shí)體內(nèi)容或信息烛缔。這樣我們就可以使用這個(gè)類(lèi)的方法來(lái)讀取整個(gè)字符或字節(jié)數(shù)據(jù)內(nèi)容馏段,而不是直接操作java.io.InputStream
。然而践瓷,EntityUtils
的使用是非常不推薦的院喜,除非響應(yīng)實(shí)體是來(lái)源于一個(gè)受信的HTTP服務(wù)器并且內(nèi)容的長(zhǎng)度是有限的。
public void chapter1_1_6() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
long len = entity.getContentLength();
if (len != -1 && len < 2048) {
System.out.println(EntityUtils.toString(entity));
} else {
// Stream content out
// content length is too large
}
}
} finally {
response.close();
}
}
在某些情況下晕翠,我們需要重復(fù)讀取實(shí)體內(nèi)容喷舀。此時(shí),實(shí)體內(nèi)容必須用某種方式來(lái)緩沖淋肾,或者在內(nèi)容或者在磁盤(pán)中硫麻。完成緩存的最簡(jiǎn)單方式就是把原始的實(shí)體用BufferedHttpEntity
類(lèi)來(lái)包裝。這能夠使原來(lái)的實(shí)體被讀進(jìn)內(nèi)存的緩存區(qū)中樊卓。
CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
1.1.7 生產(chǎn)實(shí)體內(nèi)容
HttpClient提供一些類(lèi)拿愧,這些類(lèi)用來(lái)高效地將實(shí)體內(nèi)容通過(guò)HTTP連接輸出到流。這些類(lèi)的實(shí)例能夠與包含實(shí)體的請(qǐng)求關(guān)聯(lián), 如POST
和PUT
碌尔。HttpClient提供一些類(lèi)來(lái)作為最常見(jiàn)的數(shù)據(jù)的容器浇辜,如字符串、字節(jié)數(shù)組唾戚、輸入流和文件:StringEntity
奢赂、ByteArrayEntity
、InputStreamEntity
和FileEntity
颈走。
public void chapter1_1_7() throws Exception {
File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file, ContentType.create("text/plain", "UTF-8"));
HttpPost httpPost = new HttpPost("http://localhost/action.do");
httpPost.setEntity(entity);
}
請(qǐng)注意InputStreamEntity
不是可重復(fù)的,因?yàn)樗荒軓牡讓訑?shù)據(jù)流中讀取一次咱士。通常推薦去實(shí)現(xiàn)一個(gè)自定義的HttpEntity
立由,使其成為self-contained
類(lèi)型的,而不是去使用InputStreamEntity
序厉。FileEntity
就是一個(gè)很好的例子锐膜。
1.1.7.1 HTML表單
很多應(yīng)用需要去模擬提交HTML表單的過(guò)程,例如弛房,為了登陸或提交輸入數(shù)據(jù)道盏。HttpClient提供實(shí)體類(lèi)UrlEncodedFormEntity
來(lái)幫助這個(gè)提交過(guò)程。
public void chapter1_1_7_1() throws Exception {
List<NameValuePair> formParams = new ArrayList<>();
formParams.add(new BasicNameValuePair("param1", "value1"));
formParams.add(new BasicNameValuePair("param2", "value2"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
HttpPost httpPost = new HttpPost("http://localhost/handle.do");
httpPost.setEntity(entity);
}
UrlEncodedFormEntity
會(huì)使用URL編碼來(lái)對(duì)參數(shù)進(jìn)行編碼文捶,并且產(chǎn)生如下內(nèi)容:
param1=value1¶m2=value2
1.1.7.2 內(nèi)容分塊(Content chunking)
通常推薦讓HttpClient基于傳輸?shù)腍TTP報(bào)文的屬性去選擇使用最合適的傳輸編碼荷逞。然而,通過(guò)設(shè)置HttpEntity#setChunked()
為true
來(lái)通知HttpClient使用chunk
編碼是可能的粹排。當(dāng)然种远,這僅僅只是一個(gè)提示而已。如果使用不支持chunk
編碼的HTTP協(xié)議顽耳,如HTTP/1.0
坠敷,該值將會(huì)被忽略妙同。
1.1.8 響應(yīng)處理器(Response handlers)
最簡(jiǎn)單并且最方便的方式去處理響應(yīng)是使用ResponseHandler
接口,該接口包含handleResponse(HttpResponse response)
方法膝迎。該方法完全地把用戶從連接管理中解放出來(lái)粥帚。當(dāng)使用ResponseHandler
,HttpClient會(huì)自動(dòng)地的確保連接會(huì)被釋放回給連接管理器限次,不管執(zhí)行請(qǐng)求是否成功或異常芒涡。
public void chapter1_1_8() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
ResponseHandler<MyJsonObject> rh = response -> {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
if (Objects.isNull(entity)) {
throw new ClientProtocolException("Response contains no content");
}
Gson gson = new GsonBuilder().create();
ContentType contentType = ContentType.getOrDefault(entity);
Charset charset = contentType.getCharset();
Reader reader = new InputStreamReader(entity.getContent(), charset);
return gson.fromJson(reader, MyJsonObject.class);
};
MyJsonObject myJsonObject = httpClient.execute(httpGet, rh);
}
static class MyJsonObject {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}