1.1 請(qǐng)求執(zhí)行
HttpClient 最重要的功能是執(zhí)行 HTTP 方法。執(zhí)行 HTTP 方法涉及一個(gè)或多個(gè) HTTP 請(qǐng)求/響應(yīng) 交換盟萨,通常由 HttpClient 內(nèi)部處理。
用戶期望提供一個(gè)請(qǐng)求對(duì)象來(lái)執(zhí)行兰伤,并且希望 HttpClient 將請(qǐng)求發(fā)送到目標(biāo)服務(wù)器返回相應(yīng)的響應(yīng)對(duì)象内颗,如果執(zhí)行失敗則拋出異常。
很自然敦腔,HttpClient API 的主要入口點(diǎn)是定義上述協(xié)議的 HttpClient 接口均澳。
以下是一個(gè)最基本的請(qǐng)求執(zhí)行過(guò)程的例子:
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
<...>
} finally {
response.close();
}
1.1.1 HTTP 請(qǐng)求
所有 HTTP 請(qǐng)求都有一個(gè)請(qǐng)求行,包括方法名稱(chēng)符衔,請(qǐng)求 URI 和 HTTP 協(xié)議版本负懦。
HttpClient 支持了在 HTTP / 1.1 規(guī)范中定義的所有 HTTP 方法:GET
,HEAD
柏腻,POST
纸厉,PUT
,DELETE
五嫂,TRACE
和 OPTIONS
颗品。
對(duì)于每個(gè)方法類(lèi)型,都有一個(gè)特定的類(lèi)來(lái)支持:HttpGet
沃缘, HttpHead
躯枢,HttpPost
, HttpPut
槐臀,HttpDelete
锄蹂, HttpTrace
,和 HttpOptions
水慨。
Request-URI 是統(tǒng)一資源標(biāo)識(shí)符得糜,用于標(biāo)識(shí)應(yīng)用請(qǐng)求的資源。HTTP 請(qǐng)求 URI 由協(xié)議方案晰洒,主機(jī)名朝抖,可選端口,資源路徑谍珊,可選查詢(xún)和可選片段組成治宣。
HttpGet httpget = new HttpGet(
"http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient 提供 URIBuilder
實(shí)用工具類(lèi)來(lái)簡(jiǎn)化請(qǐng)求 URI 的創(chuàng)建和修改。
stdout>
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
URI uri = new URIBuilder()
.setScheme("http")
.setHost("www.google.com")
.setPath("/search")
.setParameter("q", "httpclient")
.setParameter("btnG", "Google Search")
.setParameter("aq", "f")
.setParameter("oq", "")
.build();
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());
1.1.2 HTTP 響應(yīng)
HTTP 響應(yīng)是在接收和解釋請(qǐng)求消息之后由服務(wù)器發(fā)送回客戶端的消息砌滞。該消息的第一行包括協(xié)議版本侮邀,后跟數(shù)字狀態(tài)代碼及其關(guān)聯(lián)的文本短語(yǔ)。
stdout >
HTTP/1.1
200
OK
HTTP/1.1 200 OK
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());
1.1.3 處理消息頭
HTTP 消息可以包含描述消息的屬性的多個(gè)標(biāo)題贝润,例如內(nèi)容長(zhǎng)度绊茧,內(nèi)容類(lèi)型等。HttpClient 提供了檢索题暖,添加按傅,刪除和枚舉消息頭的方法。
stdout >
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
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=\"/\", c3=c; 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);
獲取給定類(lèi)型的所有標(biāo)頭的最有效的方法是使用 HeaderIterator 接口胧卤。
stdout >
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
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=\"/\", c3=c; domain=\"localhost\"");
HeaderIterator it = response.headerIterator("Set-Cookie");
while (it.hasNext()) {
System.out.println(it.next());
}
它還提供了便捷的方法來(lái)將 HTTP 消息解析為單獨(dú)的頭元素唯绍。
stdout >
c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost
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=\"/\",c3=c;domain=\"localhost\"");
HeaderElementIterator heit = new BasicHeaderElementIterator(
response.headerIterator("Set-Cookie"));
while (heit.hasNext()) {
HeaderElement element = heit.nextElement();
System.out.println(element.getName() + "=" + element.getValue());
NameValuePair[] params = element.getParameters();
for (int i = 0; i < params.length; i++) {
System.out.println("" + params[i]);
}
}
1.1.4 HTTP 實(shí)體
HTTP 消息可以攜帶與請(qǐng)求或響應(yīng)相關(guān)聯(lián)的內(nèi)容實(shí)體。實(shí)體可以在某些請(qǐng)求和響應(yīng)中找到枝誊,因?yàn)樗鼈兪强蛇x的况芒。
使用實(shí)體的請(qǐng)求被稱(chēng)為實(shí)體封裝請(qǐng)求。
HTTP 規(guī)范定義了兩個(gè)實(shí)體封裝請(qǐng)求的方法:POST 和 PUT叶撒。
HTTP 響應(yīng)通常期望包含有內(nèi)容實(shí)體绝骚。
當(dāng)然也有例外的情況,例如響應(yīng)為 204 No Content祠够, 304 Not Modified压汪,205 Reset Content 的時(shí)候不包含內(nèi)容實(shí)體。
HttpClient 根據(jù)其內(nèi)容來(lái)源區(qū)分三種實(shí)體:
- 流式傳輸(streamed):內(nèi)容是從流中接收到的古瓤,或者即時(shí)生成止剖。特別地,該類(lèi)別包括從 HTTP 響應(yīng)接收到的實(shí)體落君。流式實(shí)體通常不可重復(fù)穿香。
- 自包含(self-contained):內(nèi)容在內(nèi)存中或通過(guò)獨(dú)立于連接或其他實(shí)體的方式獲取。自包含的實(shí)體通常是可重復(fù)的绎速。這種類(lèi)型的實(shí)體將主要用于封閉 HTTP 請(qǐng)求的實(shí)體皮获。
- 包裝(wrapping):內(nèi)容是從另一個(gè)實(shí)體獲得的。
當(dāng)從 HTTP 響應(yīng)流出內(nèi)容時(shí)纹冤,這個(gè)區(qū)別對(duì)于連接管理很重要洒宝。對(duì)于由應(yīng)用程序創(chuàng)建并且僅使用 HttpClient 發(fā)送的請(qǐng)求實(shí)體,流和自包含的區(qū)別并不是重要萌京。因而可以通過(guò)是否可重復(fù)來(lái)區(qū)分它們待德,將不可重復(fù)的實(shí)體視為流式的,將可重復(fù)的實(shí)體視為自包含的枫夺。
1.1.4.1 可重復(fù)的實(shí)體
一個(gè)實(shí)體可以重復(fù)将宪,這意味著它的內(nèi)容可以被讀取多次。這種情況僅僅可能出現(xiàn)在自包含的實(shí)體中(例如 ByteArrayEntity 或 StringEntity)橡庞。
1.1.4.2 使用 HTTP 實(shí)體
由于實(shí)體可以表示二進(jìn)制和字符內(nèi)容较坛,因此它支持字符編碼(為了支持字符內(nèi)容)。
當(dāng)執(zhí)行帶有封閉內(nèi)容的請(qǐng)求時(shí)扒最,或者當(dāng)請(qǐng)求成功后使用響應(yīng)主體將結(jié)果發(fā)送回客戶端時(shí)丑勤,實(shí)體被創(chuàng)建。
要從實(shí)體讀取內(nèi)容吧趣,可以通過(guò) HttpEntity#getContent()
方法來(lái)檢索輸入流法竞,該方法返回一個(gè) java.io.InputStream
耙厚,或者可以向 HttpEntity#writeTo(OutputStream)
方法提供輸出流,一旦所有內(nèi)容都被寫(xiě)入給定流岔霸,該方法將返回薛躬。
當(dāng)實(shí)體和傳入消息已經(jīng)被接收,方法 HttpEntity#getContentType()
和
HttpEntity#getContentLength()
可用于讀取公共 metadata呆细,例如 Content-Type 與 Content-Length 報(bào)頭(如果可用的話)型宝。
Content-Type 報(bào)頭可以包含 text/plain
或 text/html
等文本 MIME 類(lèi)型的字符編碼,方法 HttpEntity#getContentEncoding()
可以用于讀取此信息絮爷。
如果報(bào)頭不可用趴酣,則返回 Content-Length 為 -1,Content-Type 為 NULL坑夯。如果 Content-Type 報(bào)頭可用岖寞,將返回一個(gè) Header 對(duì)象。
當(dāng)為外發(fā)消息創(chuàng)建實(shí)體時(shí)柜蜈,該 metadata 必須由實(shí)體的創(chuàng)建者來(lái)提供慎璧。
stdout >
Content-Type: text/plain; charset=utf-8
17
important message
17
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);
1.1.5 確保釋放低級(jí)別的資源
為了確保正確釋放系統(tǒng)資源,必須關(guān)閉與實(shí)體相關(guān)聯(lián)的內(nèi)容流或者響應(yīng)本身跨释。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
try {
//做一些有用的事情
} finally {
instream.close();
}
}
} finally {
response.close();
}
關(guān)閉內(nèi)容流和關(guān)閉響應(yīng)之間的區(qū)別是胸私,前者將嘗試通過(guò)消費(fèi)實(shí)體內(nèi)容來(lái)保持底層的連接,而后者會(huì)立即關(guān)閉并丟棄當(dāng)前連接鳖谈。
請(qǐng)注意岁疼,一旦方法 HttpEntity#writeTo(OutputStream)
的實(shí)體完全寫(xiě)出,也需要確保合適的釋放系統(tǒng)資源缆娃。如果這個(gè)方法通過(guò)調(diào)用 HttpEntity#getContent()
獲取一個(gè) java.io.InputStream
實(shí)例捷绒,那么也應(yīng)該在 finally 子句中關(guān)閉流。
當(dāng)處理流實(shí)體時(shí)贯要,可以使用 EntityUtils#consume(HttpEntity)
方法來(lái)確保實(shí)體內(nèi)容已被完全消耗暖侨,并且底層流已經(jīng)被關(guān)閉。
然而崇渗,可能會(huì)有一種情況字逗,當(dāng)僅需要檢索整個(gè)響應(yīng)內(nèi)容的一小部分時(shí),消耗剩余內(nèi)容并使連接可重用的性能損失太高宅广,這時(shí)可以通過(guò)關(guān)閉響應(yīng)來(lái)終止內(nèi)容流葫掉。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream instream = entity.getContent();
int byteOne = instream.read();
int byteTwo = instream.read();
//不需要休息
}
} finally {
response.close();
}
連接不會(huì)被重用,但由其持有的所有級(jí)別的資源將會(huì)被正確地釋放跟狱。
1.1.6 消費(fèi)實(shí)體內(nèi)容
消費(fèi)實(shí)體內(nèi)容推薦使用 HttpEntity#getContent()
或 HttpEntity#writeTo(OutputStream)
方法俭厚。
HttpClient 還附帶了 EntityUtils
類(lèi),它暴露了幾種靜態(tài)方法驶臊,來(lái)方便我們從實(shí)體中讀取內(nèi)容或信息挪挤〉鸪螅可以通過(guò)使用這個(gè)類(lèi)的方法取回整個(gè)內(nèi)容體的字符串或者字節(jié)數(shù)組,而不是通過(guò)去直接讀取 java.io.InputStream
來(lái)獲取內(nèi)容扛门。但是鸠信,除非響應(yīng)實(shí)體來(lái)自受信任的 HTTP 服務(wù)器,并且已知其長(zhǎng)度有限尖飞,否則強(qiáng)烈建議不要使用 EntityUtils
的方法症副。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
HttpEntity entity = response.getEntity();
if (entity != null) {
long len = entity.getContentLength();
if (len != -1 && len < 2048) {
System.out.println(EntityUtils.toString(entity));
} else {
//流內(nèi)容
}
}
} finally {
response.close();
}
在某些情況下店雅,可能需要多次讀取實(shí)體內(nèi)容政基。在這種情況下,實(shí)體內(nèi)容必須以某種方式緩存闹啦,無(wú)論是在內(nèi)存還是在磁盤(pán)上沮明。
最簡(jiǎn)單的方式是通過(guò)使用 BufferedHttpEntity
類(lèi)包裝原始實(shí)體。這會(huì)使原始實(shí)體的內(nèi)容被讀入內(nèi)存緩沖區(qū)窍奋。在所有其他方式中荐健,實(shí)體包裝器將具有原始內(nèi)容。
CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
1.1.7 創(chuàng)建實(shí)體內(nèi)容
HttpClient 提供了幾個(gè)類(lèi)琳袄,可以通過(guò) HTTP 連接高效地流出內(nèi)容江场。這些類(lèi)的實(shí)例可以將那些需要包裝其關(guān)聯(lián)的實(shí)體內(nèi)容的 HTTP 請(qǐng)求(例如 POST 和 PUT)包裝實(shí)體去流出 HTTP 請(qǐng)求端。HttpClient 為最常見(jiàn)的數(shù)據(jù)的容器(如字符串窖逗,字節(jié)數(shù)組址否,輸入流和文件)提供了幾個(gè)類(lèi):StringEntity
,ByteArrayEntity
碎紊,InputStreamEntity
和 FileEntity
佑附。
請(qǐng)注意 InputStreamEntity 不可重復(fù),因?yàn)樗荒軓幕A(chǔ)數(shù)據(jù)流中讀取一次仗考。通常建議實(shí)現(xiàn)一個(gè)自包含 HttpEntity 的自定義類(lèi)音同,而不是使用一般的 InputStreamEntity。 FileEntity 可以是一個(gè)很好的開(kāi)始秃嗜。
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);
1.1.7.1 HTML 表單
許多應(yīng)用程序需要模擬提交 HTML 表單的過(guò)程权均,例如,登錄到 Web 應(yīng)用程序或提交輸入數(shù)據(jù)锅锨。HttpClient 提供實(shí)體類(lèi) UrlEncodedFormEntity
來(lái)簡(jiǎn)化這個(gè)過(guò)程螺句。
本 UrlEncodedFormEntity
實(shí)例將使用所謂的 URL 編碼參數(shù)進(jìn)行編碼并生成以下內(nèi)容:
param1=value1¶m2=value2
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
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/handler.do");
httppost.setEntity(entity);
1.1.7.2 內(nèi)容分塊
一般建議讓 HttpClient 根據(jù)正在傳輸?shù)?HTTP 消息的屬性選擇最合適的傳輸編碼。然而橡类,可以通知 HttpClient蛇尚,通過(guò)設(shè)置 HttpEntity#setChunked()
為 true,優(yōu)先選擇塊編碼顾画。
請(qǐng)注意取劫,HttpClient 只會(huì)使用此標(biāo)志作為提示匆笤。當(dāng)使用不支持塊編碼的 HTTP 協(xié)議版本(如 HTTP/1.0)時(shí),此值將被忽略谱邪。
StringEntity entity = new StringEntity("important message",
ContentType.create("plain/text", Consts.UTF_8));
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);
1.1.8 響應(yīng)處理程序
處理響應(yīng)的最簡(jiǎn)單和最方便的方法是使用 ResponseHandler 接口炮捧,它包含 handleResponse(HttpResponse response)
方法。該方法完全消除用戶對(duì)連接管理的擔(dān)心惦银。
使用 ResponseHandler
時(shí)咆课,無(wú)論請(qǐng)求執(zhí)行成功還是出現(xiàn)異常,HttpClient 都將自動(dòng)保證將該連接釋放回連接管理器扯俱。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");
ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {
@Override
public JsonObject handleResponse(final HttpResponse response) throws IOException {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(
statusLine.getStatusCode(),
statusLine.getReasonPhrase());
}
if (entity == null) {
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 myjson = client.execute(httpget, rh);
1.2 HttpClient 接口
HttpClient 接口代表 HTTP 請(qǐng)求執(zhí)行最基本的協(xié)議书蚪。它對(duì)請(qǐng)求執(zhí)行過(guò)程沒(méi)有施加任何限制或特定細(xì)節(jié),并將連接管理迅栅,狀態(tài)管理殊校,認(rèn)證和重定向處理的具體細(xì)節(jié)留給一個(gè)具體實(shí)現(xiàn)。這使得可以更容易使用附加功能(如響應(yīng)內(nèi)容緩存)來(lái)裝飾接口读存。
一般來(lái)說(shuō)为流,HttpClient 實(shí)現(xiàn)作為一個(gè)特殊目的處理程序或策略接口實(shí)現(xiàn)的立面,負(fù)責(zé)處理 HTTP 協(xié)議的特定方面让簿,如重定向或身份驗(yàn)證處理或決定連接持久性并保持活動(dòng)持續(xù)時(shí)間敬察。這使得用戶能夠選擇性地將這些方面的默認(rèn)實(shí)現(xiàn)替換為自己的實(shí)現(xiàn)或者為特定應(yīng)用程序設(shè)計(jì)的實(shí)現(xiàn)。
ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(
HttpResponse response,
HttpContext context) {
long keepAlive = super.getKeepAliveDuration(response, context);
if (keepAlive == -1) {
// 如果 keep-alive 的值沒(méi)有被服務(wù)器顯式設(shè)置尔当,就設(shè)置為 5 秒
keepAlive = 5000;
}
return keepAlive;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setKeepAliveStrategy(keepAliveStrat)
.build();
1.2.1 HttpClient 線程安全
HttpClient 的實(shí)現(xiàn)是線程安全的莲祸。因此建議將此類(lèi)的同一個(gè)實(shí)例重用于多個(gè)請(qǐng)求執(zhí)行。
1.2.2 HttpClient 資源釋放
當(dāng)一個(gè) CloseableHttpClient 實(shí)例不再需要并且即將超出作用域時(shí)居凶,與它關(guān)聯(lián)的連接管理器必須通過(guò)調(diào)用 CloseableHttpClient#close()
方法來(lái)關(guān)閉虫给。
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
<...>
} finally {
httpclient.close();
}
1.3 HTTP 執(zhí)行上下文
最初 HTTP 被設(shè)計(jì)為無(wú)狀態(tài)的,請(qǐng)求-響應(yīng)的協(xié)議侠碧。然而抹估,現(xiàn)實(shí)世界的應(yīng)用程序通常需要通過(guò)幾個(gè)邏輯相關(guān)的請(qǐng)求-響應(yīng)交換來(lái)保持狀態(tài)信息。
為了使應(yīng)用程序能夠保持處理狀態(tài)弄兜,HttpClient 允許在特定執(zhí)行上下文(稱(chēng)為 HTTP context)中執(zhí)行 HTTP 請(qǐng)求药蜻。
如果在連續(xù)請(qǐng)求之間重復(fù)使用相同的上下文,則多個(gè)邏輯相關(guān)請(qǐng)求可以參與邏輯會(huì)話替饿。HTTP 上下文功能類(lèi)似于一個(gè) java.util.Map<String, Object>
县忌。它只是一個(gè)任意命名值的集合乔外。應(yīng)用程序可以在請(qǐng)求執(zhí)行之前填充上下文屬性底循,或者在執(zhí)行完成后檢查上下文嘉裤。
HttpContext 可以包含任意對(duì)象,因此可能不安全地在多個(gè)線程之間共享。建議每個(gè)執(zhí)行線程維護(hù)自己的上下文惋砂。
在 HTTP 請(qǐng)求執(zhí)行過(guò)程中妒挎,HttpClient 將以下屬性添加到執(zhí)行上下文中:
-
HttpConnection
表示與目標(biāo)服務(wù)器的實(shí)際連接的實(shí)例。 -
HttpHost
表示連接目標(biāo)的實(shí)例西饵。 -
HttpRoute
表示完整的連接路由的實(shí)例 -
HttpRequest
表示實(shí)際 HTTP 請(qǐng)求的實(shí)例酝掩。執(zhí)行上下文中的最終 HttpRequest 對(duì)象總是表示消息的狀態(tài)與發(fā)送到目標(biāo)服務(wù)器的狀態(tài)完全相同。默認(rèn) HTTP / 1.0 和 HTTP / 1.1 使用相對(duì)請(qǐng)求 URI眷柔。然而期虾,如果請(qǐng)求是通過(guò)非隧道模式下的代理發(fā)送的,則 URI 將是絕對(duì)地址驯嘱。 -
HttpResponse
表示實(shí)際的 HTTP 響應(yīng)镶苞。 -
java.lang.Boolean
表示實(shí)際請(qǐng)求是否被完全發(fā)送到連接目標(biāo)的標(biāo)志的對(duì)象。 -
RequestConfig
表示實(shí)際請(qǐng)求配置的對(duì)象宙拉。 -
java.util.List<URI>
表示在請(qǐng)求執(zhí)行過(guò)程中接收到的所有重定向位置的集合的對(duì)象宾尚。
可以使用 HttpClientContext 適配器類(lèi)來(lái)簡(jiǎn)化與上下文狀態(tài)的分離丙笋。
HttpContext context = <...>
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();
表示邏輯相關(guān)會(huì)話的多個(gè)請(qǐng)求序列應(yīng)該使用相同的 HttpContext 實(shí)例執(zhí)行谢澈,以確保在請(qǐng)求之間自動(dòng)傳播會(huì)話上下文和狀態(tài)信息。
在以下示例中御板,由初始請(qǐng)求設(shè)置的請(qǐng)求配置將保留在執(zhí)行上下文中锥忿,并被傳播到共享相同上下文的連續(xù)請(qǐng)求。
CloseableHttpClient httpclient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(1000)
.setConnectTimeout(1000)
.build();
HttpGet httpget1 = new HttpGet("http://localhost/1");
httpget1.setConfig(requestConfig);
CloseableHttpResponse response1 = httpclient.execute(httpget1, context);
try {
HttpEntity entity1 = response1.getEntity();
} finally {
response1.close();
}
HttpGet httpget2 = new HttpGet("http://localhost/2");
CloseableHttpResponse response2 = httpclient.execute(httpget2, context);
try {
HttpEntity entity2 = response2.getEntity();
} finally {
response2.close();
}
1.4 HTTP 協(xié)議攔截器
HTTP 協(xié)議攔截器是一個(gè)實(shí)現(xiàn) HTTP 協(xié)議特定切面的程序怠肋。通常協(xié)議攔截器期望作用于輸入消息的一個(gè)特定頭部或一組相關(guān)頭部敬鬓,或者使用一個(gè)特定頭部或一組相關(guān)頭部填充傳出的消息。
協(xié)議攔截器還可以操縱包含消息的內(nèi)容實(shí)體 - 顯式內(nèi)容壓縮/解壓縮就是一個(gè)很好的例子笙各。通常這是通過(guò)使用“裝飾器”模式來(lái)實(shí)現(xiàn)的钉答,其中包裝器實(shí)體類(lèi)用于裝飾原始實(shí)體。幾個(gè)協(xié)議攔截器可以組合形成一個(gè)邏輯單元杈抢。
協(xié)議攔截器可以通過(guò) HTTP 執(zhí)行上下文共享信息(如處理狀態(tài))進(jìn)行協(xié)作数尿。協(xié)議攔截器可以使用 HTTP 上下文來(lái)存儲(chǔ)一個(gè)請(qǐng)求或多個(gè)連續(xù)請(qǐng)求的處理狀態(tài)。通常執(zhí)行攔截器的順序并不重要惶楼,只要它們不依賴(lài)執(zhí)行上下文的特定狀態(tài)即可右蹦。如果協(xié)議攔截器具有相互依賴(lài)關(guān)系,那么必須以特定順序執(zhí)行歼捐,應(yīng)將它們按照與其預(yù)期執(zhí)行順序相同的順序添加到協(xié)議處理器中何陆。
協(xié)議攔截器必須實(shí)現(xiàn)為線程安全。與 servlet 類(lèi)似豹储,協(xié)議攔截器不應(yīng)該使用實(shí)例變量贷盲,除非對(duì)這些變量的訪問(wèn)是同步(synchronized)的。
下面是一個(gè)例子剥扣,說(shuō)明如何使用本地上下文來(lái)持續(xù)連續(xù)請(qǐng)求之間的處理狀態(tài):
CloseableHttpClient httpclient = HttpClients.custom()
.addInterceptorLast(new HttpRequestInterceptor() {
public void process(
final HttpRequest request,
final HttpContext context) throws HttpException, IOException {
AtomicInteger count = (AtomicInteger) context.getAttribute("count");
request.addHeader("Count", Integer.toString(count.getAndIncrement()));
}
})
.build();
AtomicInteger count = new AtomicInteger(1);
HttpClientContext localContext = HttpClientContext.create();
localContext.setAttribute("count", count);
HttpGet httpget = new HttpGet("http://localhost/");
for (int i = 0; i < 10; i++) {
CloseableHttpResponse response = httpclient.execute(httpget, localContext);
try {
HttpEntity entity = response.getEntity();
} finally {
response.close();
}
}
1.5 異常處理
HTTP 協(xié)議處理器可以拋出兩種類(lèi)型的異常:java.io.IOException
表示 I/O 故障(例如套接字超時(shí)或套接字復(fù)位)巩剖,而 HttpException
表示 HTTP 失敗慨灭,例如違反 HTTP 協(xié)議。通常 I/O 錯(cuò)誤被認(rèn)為是非致命和可恢復(fù)的球及,而 HTTP 協(xié)議錯(cuò)誤被認(rèn)為是致命的氧骤,不能自動(dòng)恢復(fù)。
請(qǐng)注意吃引,HttpClient 實(shí)現(xiàn)重新拋出 HttpException 作為 ClientProtocolException筹陵,它是一個(gè) java.io.IOException 的子類(lèi) 。這使得 HttpClient 的用戶能夠在單個(gè) catch 子句同時(shí)處理 I/O 錯(cuò)誤和協(xié)議錯(cuò)誤镊尺。
1.5.1 HTTP 傳輸安全
重要的是要了解 HTTP 協(xié)議并不適合所有類(lèi)型的應(yīng)用程序朦佩。HTTP 是一種簡(jiǎn)單的面向 請(qǐng)求/響應(yīng) 的協(xié)議,最初被設(shè)計(jì)為支持靜態(tài)或動(dòng)態(tài)生成的內(nèi)容檢索庐氮。它從來(lái)沒(méi)有意圖支持事務(wù)性操作语稠。
例如:
- 如果 HTTP 服務(wù)器成功接收和處理請(qǐng)求,生成響應(yīng)并將狀態(tài)代碼發(fā)送回客戶端弄砍,則 HTTP 服務(wù)器將考慮其部分合同仙畦。
- 如果客戶端由于讀取超時(shí),請(qǐng)求取消或系統(tǒng)崩潰而無(wú)法全部收到響應(yīng)音婶,服務(wù)器將不會(huì)嘗試回滾事務(wù)慨畸。
- 如果客戶端決定重試相同的請(qǐng)求,服務(wù)器將不可避免地最終不止一次地執(zhí)行相同的事務(wù)衣式。
在某些情況下寸士,這可能導(dǎo)致應(yīng)用程序數(shù)據(jù)損壞或應(yīng)用程序狀態(tài)不一致。
即使 HTTP 從未被設(shè)計(jì)為支持事務(wù)處理碴卧,但是如果滿足某些條件弱卡,則仍然可以將其用作任務(wù)關(guān)鍵應(yīng)用程序的傳輸協(xié)議。
為了確保 HTTP 傳輸層的安全住册,系統(tǒng)必須確保應(yīng)用層上的 HTTP 方法的冪等性婶博。
1.5.2 冪等方法
HTTP/1.1
規(guī)范定義了一種冪等方法(方法也可以具有“冪等”的屬性(除了錯(cuò)誤或到期問(wèn)題),N > 0 相同請(qǐng)求的副作用與單個(gè)請(qǐng)求相同)界弧。換句話說(shuō)凡蜻,應(yīng)用程序應(yīng)該確保它準(zhǔn)備好處理多次執(zhí)行相同方法的含義。
這可以通過(guò)例如提供唯一的事務(wù) ID 和避免執(zhí)行相同邏輯操作的其他手段來(lái)實(shí)現(xiàn)垢箕。
請(qǐng)注意划栓,這個(gè)問(wèn)題不是 HttpClient 特有的√趸瘢基于瀏覽器的應(yīng)用程序受到與 HTTP 方法非冪等性相關(guān)的完全相同的問(wèn)題忠荞。
默認(rèn)情況下,為了兼容性的原因,HttpClient 僅假定非實(shí)體封閉方法委煤,例如 GET 或者 HEAD 方法 是冪等的堂油;實(shí)體封閉方法,例如 POST 或者 PUT 不是冪等的碧绞。
1.5.3 自動(dòng)異掣颍恢復(fù)
默認(rèn)情況下,HttpClient 嘗試自動(dòng)從 I/O 異臣チ冢恢復(fù)迫靖。默認(rèn)的自動(dòng)恢復(fù)機(jī)制僅限于一些已知是安全的異常。
HttpClient 將不會(huì)嘗試從任何邏輯或 HTTP 協(xié)議錯(cuò)誤(派生自 HttpException 類(lèi))中恢復(fù)兴使。
HttpClient 會(huì)自動(dòng)重試那些假定為冪等的方法系宜。
當(dāng) HTTP 請(qǐng)求仍被傳送到目標(biāo)服務(wù)器(即請(qǐng)求尚未完全傳輸?shù)椒?wù)器)時(shí),HttpClient 將自動(dòng)重試那些失敗的傳輸異常的方法发魄。
1.5.4 請(qǐng)求重試處理程序
為了啟用自定義異稠锬粒恢復(fù)機(jī)制,應(yīng)該提供 HttpRequestRetryHandler
接口的實(shí)現(xiàn)励幼。
請(qǐng)注意汰寓,可以使用 StandardHttpRequestRetryHandler
替代默認(rèn)使用的一個(gè),以匹配由 RFC-2616 定義為冪等安全要求的那些方法來(lái)自動(dòng)重試:GET赏淌,HEAD踩寇,PUT啄清,DELETE六水,OPTIONS 和 TRACE。
HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
public boolean retryRequest(
IOException exception,
int executionCount,
HttpContext context) {
if (executionCount >= 5) {
//如果超過(guò)最大重試計(jì)數(shù)辣卒,請(qǐng)不要重試
return false;
}
if (exception instanceof InterruptedIOException) {
// 時(shí)間到
return false;
}
if (exception instanceof UnknownHostException) {
// 未知主機(jī)
return false;
}
if (exception instanceof ConnectTimeoutException) {
// 拒絕連接
return false;
}
if (exception instanceof SSLException) {
// SSL握手異常
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
//如果請(qǐng)求被認(rèn)為是冪等掷贾,則重試
return true;
}
return false;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setRetryHandler(myRetryHandler)
.build();
1.6 中止請(qǐng)求
在某些情況下,由于目標(biāo)服務(wù)器的負(fù)載過(guò)高或在客戶端發(fā)出的太多并發(fā)請(qǐng)求荣茫,HTTP 請(qǐng)求執(zhí)行在預(yù)期時(shí)間內(nèi)無(wú)法完成想帅。在這種情況下,可能需要提前終止請(qǐng)求啡莉,并在 I/O 操作中解除阻塞執(zhí)行線程港准。
HttpClient 執(zhí)行的 HTTP 請(qǐng)求可以通過(guò)調(diào)用 HttpUriRequest#abort()
方法在執(zhí)行的任何階段中止 。該方法是線程安全的咧欣,可以從任何線程調(diào)用浅缸。
當(dāng)一個(gè) HTTP 請(qǐng)求中止時(shí),它的執(zhí)行線程 - 即使當(dāng)前在 I/O 操作中被阻塞 - 也可以通過(guò)拋出一個(gè) InterruptedIOException 來(lái)疏通阻塞線程魄咕。
1.7 重定向處理
HttpClient 會(huì)自動(dòng)處理所有類(lèi)型的重定向衩椒,除了需要用戶干預(yù)的 HTTP 規(guī)范明確禁止的重定向之外。
See Other(狀態(tài)碼 303)重定向 POST 和 PUT 請(qǐng)求轉(zhuǎn)換為 GET 請(qǐng)求作為 HTTP 規(guī)范的要求。
可以使用自定義重定向策略來(lái)放對(duì) HTTP 規(guī)范強(qiáng)加的 POST 方法的自動(dòng)重定向的限制毛萌。
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
HttpClient 經(jīng)常在其執(zhí)行過(guò)程中重寫(xiě)請(qǐng)求消息苟弛。默認(rèn)情況下,HTTP/1.0 和 HTTP/1.1 通常使用相對(duì)請(qǐng)求URI阁将。
同樣膏秫,原始請(qǐng)求可能會(huì)從一個(gè)地址重定向到另一個(gè)地址多次。最終絕對(duì) HTTP 地址定位可以使用原始請(qǐng)求和上下文構(gòu)建出來(lái)做盅。
一個(gè)實(shí)用的方法 URIUtils#resolve
可用于構(gòu)建用于生成最終 HTTP 請(qǐng)求絕對(duì)的 URI荔睹。該方法包括來(lái)自重定向請(qǐng)求或原始請(qǐng)求的最后一個(gè)片段標(biāo)識(shí)符。
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpget = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response = httpclient.execute(httpget, context);
try {
HttpHost target = context.getTargetHost();
List<URI> redirectLocations = context.getRedirectLocations();
URI location = URIUtils.resolve(httpget.getURI(), target, redirectLocations);
System.out.println("Final HTTP location: " + location.toASCIIString());
//預(yù)期是絕對(duì)的URI
} finally {
response.close();
}