1.1 執(zhí)行請求
HttpClient最基本的功能是執(zhí)行HTTP方法。一個HTTP方法的執(zhí)行涉及到一個或多個HTTP請求和響應(yīng)的交換,這通常是在HttpClient內(nèi)部處理的。用戶需要提供一個請求對象,HttpClient負(fù)責(zé)傳輸這個請求到目標(biāo)服務(wù)器并返回相對應(yīng)的響應(yīng)的對象,如果執(zhí)行不成功,則拋出異常。
HttpClient API的入口就是HttpClient接口。
以下是最簡單的形式的執(zhí)行請求的例子:
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請求
所有的HTTP請求都有一個請求頭,其中包含方法名、請求URI和HTTP協(xié)議版本號。
HttpClient支持所有HTTP/1.1規(guī)格中定義的HTTP方法,包括:GET,HEAD, POST, PUT, DELETE, TRACE 和 OPTIONS。每種方法都有與之對應(yīng)的類:HttpGet, HttpHead, HttpPost, HttpPut, HttpDelete, HttpTrace 和 HttpOptions 。
請求URI是請求資源的統(tǒng)一資源描述符(Uniform Resource Identifier)。HTTP請求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工具類來簡化創(chuà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ù)器接收和處理完請求報文后所返回的報文。報文的第一行由協(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 報文頭部
HTTP報文包含一些用于描述報文的頭部信息,如內(nèi)容長度、內(nè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
獲取所有給定類型頭部信息最有效方式是使用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報文可以攜帶與請求或響應(yīng)相關(guān)聯(lián)的內(nèi)容實(shí)體。因?yàn)閷?shí)體是可選的,并不是所有的請求和響應(yīng)中都包含實(shí)體。使用實(shí)體的請求被稱為包含實(shí)體的請求(entity enclosing request)。HTTP規(guī)格中定義了2種包含實(shí)體的請求的方法:POST 和 PUT。
響應(yīng)通常會包含一個內(nèi)容實(shí)體。但也是例外,如HEAD方法的響應(yīng)和 204 No Content, 304 Not Modified, 205 Reset Content 響應(yīng)。
HttpClient區(qū)分三種實(shí)體類型,取決于它們內(nè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ù)的。這種類型的實(shí)體最多用于包含實(shí)體的請求(entity enclosing request)。 - wrapping: 內(nèi)容從另一實(shí)體獲得。
當(dāng)從HTTP響應(yīng)中獲取數(shù)據(jù)流時,這些區(qū)分對于連接管理來說是重要的。對于應(yīng)用創(chuàng)建的請求實(shí)體,且僅使用HttpClient來發(fā)送,streamed 還是 self-contained 的區(qū)別就不重要了。這種情況下,建議把不可重復(fù)的實(shí)體歸為streamed類型,可重復(fù)的為self-contained類型。
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)建的時機(jī)有 a) 執(zhí)行包含內(nèi)容的請求; b) 請求成功后,響應(yīng)體使用實(shí)體將結(jié)果返回。
為了從輸入報文的實(shí)體中讀取內(nèi)容,我們可以通過HttpEntity#getContent()方法獲取輸入流java.io.InputStream, 或者我們可以通過HttpEntity#writeTo(OutpusStream)方法將其寫到另一個給定的輸出流中。
當(dāng)從響應(yīng)報文中接收到實(shí)體后,HttpEntity#getContentType和HttpEntity#getContentLength方法可以用來讀取通用的元數(shù)據(jù),如Content-Type和Content-Length頭部信息(如果有的話)。Content-Type頭部對于文本類型的多媒體類型(如text/plain或text/html)來說可能包含字符編碼的信息,HttpEntity#getContentEncoding()方法可能用來讀取該信息。如果頭部不可用的話,HttpEntity#getContentLength返回-1,HttpEntity#getContentType返回NULL。如果Content-Type可用的話,Header對象將會被返回。
當(dāng)給輸出報文創(chuàng)建實(shí)體,元數(shù)據(jù)必須使用實(shí)體的創(chuàng)建者方法來創(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ū)別在于,前者通過消費(fèi)實(shí)體內(nèi)容來試圖保持底層連接,后者會立即關(guān)閉并且丟棄該連接。
請注意HttpEntity#writeTo(OUtputStream)方法也需要確保正確釋放系統(tǒng)資源。如果這個方法通過調(diào)用HttpEntity#getContent方法來獲取的java.io.InputStream實(shí)例,這也需要在一個finally子句中將其關(guān)閉。
我們也可以使用EntityUtils#consume(HttpEntity)方法來確認(rèn)實(shí)體內(nèi)容被完全消費(fèi)并且底層流被關(guān)閉。
有一種情況,如果僅需要讀取響應(yīng)中的一部分內(nèi)容,并且報文剩余內(nèi)容的性能代價和保持連接的代價太高的話,我們可以通過關(guān)閉響應(yīng)來結(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();
}
}
連接將不會被重用,而且被該連接持有的所有資源將會被正確地釋放。
1.1.6 消費(fèi)實(shí)體內(nèi)容
消費(fèi)一個實(shí)體的內(nèi)容推薦的方法是使用HttpEntity#getContent() 或HttpEntity#writeTo(OutpusStream)方法。HttpClient也包含EntityUtils類,其他包含一些靜態(tài)方法可以理容易地讀取實(shí)體內(nèi)容或信息。這樣我們就可以使用這個類的方法來讀取整個字符或字節(jié)數(shù)據(jù)內(nèi)容,而不是直接操作java.io.InputStream。然而,EntityUtils的使用是非常不推薦的,除非響應(yīng)實(shí)體是來源于一個受信的HTTP服務(wù)器并且內(nèi)容的長度是有限的。
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í)體內(nèi)容必須用某種方式來緩沖,或者在內(nèi)容或者在磁盤中。完成緩存的最簡單方式就是把原始的實(shí)體用BufferedHttpEntity類來包裝。這能夠使原來的實(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提供一些類,這些類用來高效地將實(shí)體內(nèi)容通過HTTP連接輸出到流。這些類的實(shí)例能夠與包含實(shí)體的請求關(guān)聯(lián), 如POST和PUT。HttpClient提供一些類來作為最常見的數(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);
}
請注意InputStreamEntity不是可重復(fù)的,因?yàn)樗荒軓牡讓訑?shù)據(jù)流中讀取一次。通常推薦去實(shí)現(xiàn)一個自定義的HttpEntity,使其成為self-contained類型的,而不是去使用InputStreamEntity。FileEntity就是一個很好的例子。
1.1.7.1 HTML表單
很多應(yīng)用需要去模擬提交HTML表單的過程,例如,為了登陸或提交輸入數(shù)據(jù)。HttpClient提供實(shí)體類UrlEncodedFormEntity來幫助這個提交過程。
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會使用URL編碼來對參數(shù)進(jìn)行編碼,并且產(chǎn)生如下內(nèi)容:
param1=value1¶m2=value2
1.1.7.2 內(nèi)容分塊(Content chunking)
通常推薦讓HttpClient基于傳輸?shù)腍TTP報文的屬性去選擇使用最合適的傳輸編碼。然而,通過設(shè)置HttpEntity#setChunked()為true來通知HttpClient使用chunk編碼是可能的。當(dāng)然,這僅僅只是一個提示而已。如果使用不支持chunk編碼的HTTP協(xié)議,如HTTP/1.0,該值將會被忽略。
1.1.8 響應(yīng)處理器(Response handlers)
最簡單并且最方便的方式去處理響應(yīng)是使用ResponseHandler接口,該接口包含handleResponse(HttpResponse response)方法。該方法完全地把用戶從連接管理中解放出來。當(dāng)使用ResponseHandler,HttpClient會自動地的確保連接會被釋放回給連接管理器,不管執(zhí)行請求是否成功或異常。
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;
}
}