文檔翻譯自Http Client官方文檔,原文鏈接如下:
https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/fundamentals.html
第一章 基礎教程
1.1 請求執(zhí)行
HttpClient最基本的功能就是執(zhí)行HTTP方法。HTTP方法的執(zhí)行涉及到若干HTTP請求/HTTP響應的交換倾鲫,通常是在HttpClient內(nèi)部被處理的删掀。使用者需要提供一個請求對象來執(zhí)行,同時HttpClient需要將請求傳輸?shù)侥繕朔掌魃希缓蠓祷叵鄳捻憫獙ο笄绻桑蛘咴趫?zhí)行不成功的時候拋出異常。
非常自然地踊赠,HttpClient API的主要入口就是HttpClient接口呵扛,它定義了結構,就像下面描述的這樣筐带。
下面是一個請求執(zhí)行過程最簡單的例子:
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
<...>
} finally {
response.close();
}
1.1.1 HTTP請求
所有的HTTP請求都有一個請求行今穿,包含了一個方法名、一個請求URI和一個HTTP協(xié)議版本伦籍。
HttpClient支持在HTTP/1.1規(guī)范中定義的所有HTTP方法:GET
, HEAD
, POST
, PUT
, DELETE
, TRACE
和OPTION
蓝晒。有一些特定的類來對應每個方法類型:HttpGet
, HttpHead
, HttpPost
, HttpPut
, HttpDelete
, HttpTrace
和HttpOptions
。
Request-URI是Uniform Resource Identifier帖鸦,可以來識別請求所需要的資源芝薇。HTTP請求的URI包含:
- 協(xié)議模式(protocol scheme)
- 主機名(host name)
- 可選端口(optional port)
- 資源路徑(resource path)
- 可選查詢(optional query)
- 可選片段(optional fragment)
HttpGet httpget = new HttpGet(
"http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
HttpClient提供了URIBuilder
工具類來簡化請求URI的創(chuàng)建和修改。
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());
輸出 >
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
1.1.2 HTTP響應
HTTP響應是當服務器接收并解析到請求消息后作儿,發(fā)送回客戶端的消息洛二。消息的第一行包含協(xié)議版本,然后是數(shù)值型的狀態(tài)碼立倍,然后是與狀態(tài)碼相關的文本短語灭红。
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)容長度(content length)口注,內(nèi)容類型(content type)等等变擒。HttpClient提供了方法來檢索、添加寝志、移除和枚舉消息頭娇斑。
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);
輸出 >
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
2
獲取一個給定類型所有消息頭的最有效的方式就是使用HeaderIterator
接口。
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());
}
輸出 >
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
HttpClient同樣提供了將HTTP消息轉化為一個個的消息頭元素的簡便方法材部。
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 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 (int i = 0; i < params.length; i++) {
System.out.println(" " + params[i]);
}
}
輸出 >
c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost
1.1.4 HTTP實體
HTTP消息可以攜帶一個與請求或者響應相關聯(lián)的內(nèi)容實體毫缆。實體可以在一些請求和響應中被找到,因為它們是可選的乐导。使用了實體的請求被作為實體封裝的請求苦丁。HTTP規(guī)范定義了兩個實體來封裝請求方法:POST
和PUT
。響應通常會被需要封裝一個內(nèi)容實體物臂。對于這條規(guī)則有一些例外的情況旺拉,比如對HEAD
方法的響應和204 No Content
, 304 Not Modofied
, 205 Reset Content
響應。
HttpClient區(qū)分3種類型的實體棵磷,取決于它們內(nèi)容來源于哪里:
- streamed:這種內(nèi)容從一個流中被接收蛾狗,或者實時地生成。特別的一點是仪媒,這個類別包含從HTTP響應接收到的實體沉桌。流式的實體通常是不可重復的。
- self-contained:這種內(nèi)容在內(nèi)存中,或者從其它獨立于連接或者其它實體的方式中獲得留凭。自包含的實體通常是可重復的佃扼。這種類型將會更多地被那些封裝HTTP請求的實體使用。
- wrapping:這種內(nèi)容從另一個實體中獲得蔼夜。
當從一個HTTP響應中流出內(nèi)容的時候松嘶,這種區(qū)分對于連接管理來說是重要的。對于一個由應用創(chuàng)建挎扰,并且只使用HttpClient發(fā)送的請求實體來說翠订,streamed
和self-contained
這兩種之間的區(qū)別并不那么重要。在這種情況下遵倦,建議考慮將不可重復的實體作為streamed
尽超,將可重復的實體作為self-contained
。
1.1.4.1 可重復的實體
一個實體可以是可重復的梧躺,意味著它的內(nèi)容可以不止一次地被讀取似谁。只有self-contained的實體才可以是可重復的(比如ByteArrayEntity
或者StringEntity
)。
1.1.4.2 使用HTTP實體
由于一個實體可以表示二進制和字符內(nèi)容掠哥,所以它對字符編碼有一定的支持巩踏。
為了從實體中讀取內(nèi)容,可以通過HttpEntity#getContent()
方法從輸入流中檢索续搀,進而返回一個java.io.InputStream
塞琼,或者為HttpEntity#writeTo(OutputStream)
方法提供一個輸出流,進而將所有已寫入到給定的流的內(nèi)容一次性地返回禁舷。
當實體已經(jīng)接收到一個進來的消息彪杉,HttpEntity#getContentType()
方法和HttpEntity#getContentLength()
方法可以被用來讀取共同的元數(shù)據(jù),比如Content-Type
和Content-Length
消息頭(如果它們可用的話)牵咙。由于Content-Type
消息頭可以包含一個字符編碼來指定文本的mime-type派近,比如:text/plain
或者text/html
。HttpEntity#getContentEncoding()
方法被用來讀取這個信息洁桌。如果消息頭不可用渴丸,那么一個為-1
的Content-Length
將被返回,并且Content-Type
的值將為NULL
另凌。如果Content-Type
消息頭可用谱轨,那么一個Header
對象將被返回。
當為出去的消息創(chuàng)建一個實體的時候途茫,這個元數(shù)據(jù)必須被實體的創(chuàng)建者提供碟嘴。
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 確保釋放底層資源
為了確保適當?shù)蒯尫畔到y(tǒng)資源溪食,必須關閉與實體相關的內(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();
try {
// do something useful
} finally {
instream.close();
}
}
} finally {
response.close();
}
關閉內(nèi)容流和關閉響應的區(qū)別在于,前者將在消費實體內(nèi)容的時候來嘗試保持底層的連接是活動的,而后者將立即關閉并拋棄連接栅组。
請留意雀瓢,在確保“一旦實體被完全寫出玉掸,系統(tǒng)資源能夠被適當?shù)蒯尫拧钡臅r候刃麸,HttpEntity#writeTo(OutputStream)
方法是同樣需要的。如果這個方法通過調(diào)用HttpEntity#getContent()
來獲取一個java.io.InputStream
實例司浪,那么在finally語句塊中關閉流同樣是需要的泊业。
在使用流實體的時候,可以使用EntityUtils#consume(HttpEntity)
方法來確保實體的內(nèi)容已經(jīng)被完全地消耗啊易,并且底層的流已經(jīng)被關閉吁伺。
然而,有一些情況是租谈,當只有全部響應內(nèi)容中的一小部分需要被檢索的時候篮奄,消費其它的內(nè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();
// Do not need the rest
}
} finally {
response.close();
}
連接將不可被重用静汤,但是它所持有的所有級別的資源都將被正確地回收。
1.1.6 消費實體內(nèi)容
消費一個實體內(nèi)容所推薦的方式是使用它的HttpEntity#getContent()
或者HttpEntity#writeTo(OutputStream)
方法。HttpClient也使用EntityUtils類來暴露若干靜態(tài)的方法仗扬,使從一個實體中讀取內(nèi)容或者信息變得更容易〖危可以通過食用這個類中的方法來檢索整個內(nèi)容體到一個string或者byte數(shù)組执庐,而不是直接去讀java.io.InputStream
。然而酒繁,對EntityUtils
的使用是強烈不推薦的滓彰,除非響應實體來源于可信任的HTTP服務器,并且知道長度是有限的州袒。
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 {
// Stream content out
}
}
} finally {
response.close();
}
在一些情況下揭绑,能夠不止一次地讀取實體的內(nèi)容是需要的。在這種情況下郎哭,實體內(nèi)容必須被以某種方式緩存起來他匪,或者在內(nèi)存中或者在硬盤上。最簡單的做法就是通過BufferedHttpEntity
類來包裝原始的實體夸研。這將會使原始實體的內(nèi)容被讀入到內(nèi)存緩存中邦蜜。對于其它的任何方式,實體包裝都會擁有原始的那個實體亥至。
CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
1.1.7 生產(chǎn)實體內(nèi)容
HttpClient提供若干的可使用的類來更高效地通過HTTP連接來流出內(nèi)容悼沈。那些類的實體可以與實體包裝的請求(比如POST和PUT)相關聯(lián)贱迟,來將實體內(nèi)容包裝到出去的HTTP請求中。HttpClient為最常見的數(shù)據(jù)容器提供了若干的類絮供,比如string, byte數(shù)組, 輸入流和文件:StringEntity
, ByteArrayEntity
, InputStreamEntity
和FileEntity
衣吠。
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
是不可重復的,因為它只能被一次性地從底層數(shù)據(jù)流中讀取壤靶。通常來講缚俏,實現(xiàn)一個自定義的self-contained的HttpEntity
類而不使用通用InputStreamEntity
是被推薦的做法。FileEntity
會是個很好的起點贮乳。
1.1.7.1 HTML表單
許多的應用都需要模擬HTML表單提交的過程忧换,比如,為了登錄一個web應用或者提交input數(shù)據(jù)向拆。HttpClient提供了實體類UrlEncodedEntity來幫助處理包雀。
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);
UrlEncodedFormEntity
實例將使用URL encoding來編碼參數(shù),并產(chǎn)生如下的內(nèi)容:
param1=value1¶m2=value2
1.1.7.2 內(nèi)容分片
通常來說亲铡,最好讓HttpClient來選擇最適合的傳輸編碼才写,它會基于傳輸?shù)腍TTP消息的屬性。然而奖蔓,也可以通過設置HttpEntity#setChunked()為true來告訴HttpClient赞草,我們傾向于使用chunk編碼。請注意吆鹤,HttpClient將僅把這個標識作為一個提示厨疙。在不支持chunk編碼的HTTP協(xié)議版本(比如HTTP/1.0),這個值將被忽略疑务。
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 響應處理器
處理響應最簡單和最方便的方式就是使用ResponseHandler接口沾凄,它包含handleResponse(HttpResponse response)方法。這個方法將使用者從連接管理的擔心中完全解脫出來知允。當使用一個ResponseHandler的時候撒蟀,HttpClient將會自動地搞定這些,確保釋放的連接返回到連接管理器中温鸽,而不管請求是否執(zhí)行成功或者引發(fā)了異常保屯。
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請求執(zhí)行最基本的結構。它不給請求的執(zhí)行過程強加任何的限制或者特別的細節(jié)涤垫,并且對連接管理姑尺、狀態(tài)管理、認證和重定向的處理使用各自獨立的實現(xiàn)蝠猬。這將使得使用額外的功能(比如響應內(nèi)容緩存)去裝飾接口變得更加簡單切蟋。
通常來講,HttpClient
的實現(xiàn)表現(xiàn)的像許多特殊目的處理器的切面榆芦,或者策略接口的實現(xiàn)用于負責處理HTTP協(xié)議的一個特定的方面(比如柄粹,重定向喘鸟、認證處理、對連接持久化的決定镰惦、保持活動持久)。這使使用者可以有選擇的替換這些方面的默認實現(xiàn)犬绒,成為自定義的旺入,應用特有的。
ConnectionKeepAliveStrategy keepAliveStrat = new DefaultConnectionKeepAliveStrategy() {
@Override
public long getKeepAliveDuration(
HttpResponse response,
HttpContext context) {
long keepAlive = super.getKeepAliveDuration(response, context);
if (keepAlive == -1) {
// Keep connections alive 5 seconds if a keep-alive value
// has not be explicitly set by the server
keepAlive = 5000;
}
return keepAlive;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setKeepAliveStrategy(keepAliveStrat)
.build();
1.2.1 HttpClient線程安全
HttpClient
的實現(xiàn)被認為是線程安全的凯力。推薦對于多請求的執(zhí)行重用同一個這個類的實例茵瘾。
1.2.2 HttpClient資源釋放
當一個CloseableHttpClient
實例不再被需要,并且將要從與之關聯(lián)的連接管理器的范圍內(nèi)剔除咐鹤,那么它一定要通過調(diào)用CloseableHttpClient#close()
方法被關閉拗秘。
1.3 HTTP執(zhí)行上下文
原始的HTTP被設計成為一個無狀態(tài)的、面向“請求-響應”的協(xié)議祈惶。然而雕旨,真實世界的應用經(jīng)常需要能夠通過若干邏輯相關的“請求-響應”交換來保持信息的狀態(tài)。為了使應用能夠去維護一個處理狀態(tài)捧请,HttpClient允許HTTP請求在一個特定的執(zhí)行環(huán)境中被執(zhí)行凡涩,這個環(huán)境叫做HTTP上下文。多個邏輯相關的請求可以參與到一個邏輯會話中疹蛉,如果連貫的請求重用了相同的上下文活箕。HTTP上下文和java.util.Map<String, Object>
很像。它簡單來看就是一組任意的有名稱的值的集合可款。一個應用可以將上下文屬性置于請求執(zhí)行之前或者在執(zhí)行完成之后檢查上下文育韩。
HttpContext可以包含任意的對象,因此在多線程共享的時候是不安全的闺鲸。建議為每個線程的執(zhí)行維護一個它自己的上下文筋讨。
在HTTP請求執(zhí)行的過程中,HttpClient為執(zhí)行上下文添加了如下的屬性:
- HttpConnection實例表示真正的到目標服務器的連接
- HttpHost實例表示連接目標
- HttpRoute實例表示完整的連接路由
- HttpRequest實例表示真正的HTTP請求摸恍。在執(zhí)行上下文中最終的HttpRequest對象總是精確地指示著消息的狀態(tài)版仔,它被發(fā)送到目標服務器。按照默認的误墓,HTTP/1.0和HTTP/1.1使用各自的請求URI蛮粮。然而,如果請求在一個非隧道模式通過一個代理發(fā)送谜慌,那么URI將是絕對的然想。
- HttpResponse實例表示真正的HTTP響應
- java.lang.Boolean對象是一個標識,來表示真實的請求是否完全傳輸?shù)竭B接的目標
- RequestConfig對象表示真正的請求配置
- java.util.List<URI>對象表示在請求處理過程中接收到的所有重定向位置的集合欣范。
可以使用HttpClientContext適配器類來簡化與上下文狀態(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();
代表一個邏輯相關的會話的多請求序列應該在相同的HttpContext
實例中被執(zhí)行令哟,來確保請求之間會話上下文和狀態(tài)信息的自動繁殖。
在下面的例子中妨蛹,被初始化請求所設置的請求配置將在執(zhí)行上下文中被保持屏富,并且被繁殖成為連貫的共享相同上下文的請求。
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é)議攔截器是一個實現(xiàn)HTTP協(xié)議一個特定方面的實現(xiàn)的例程蛙卤。通常來講狠半,協(xié)議攔截器被要求在一個特定消息頭或者進來的消息的一組相關消息頭上發(fā)揮作用〔眩或者神年,在一個特定頭消息的出去的消息或者一組相關的頭消息上發(fā)揮作用。協(xié)議攔截器同樣可以操作那些被消息包裹的內(nèi)容實體行嗤,比如透明的內(nèi)容壓縮/解壓縮已日。通常來說,這是通過使用“裝飾器”模式來實現(xiàn)的栅屏,一個包裝實體類被用來裝飾原始的實體飘千。若干的協(xié)議攔截器可以被整合成為一個邏輯單元。
協(xié)議攔截器可以通過共享信息來協(xié)作栈雳,比如通過HTTP執(zhí)行上下文來處理狀態(tài)占婉。協(xié)議處理器能使用HTTP上下文來為一個請求或者若干連續(xù)的請求存儲一個處理狀態(tài)。
通常來說甫恩,攔截器執(zhí)行的順序應該不依賴于一個特定的執(zhí)行上下文的狀態(tài)逆济。如果協(xié)議攔截器有相互依賴,并且必須以一個特定的順序被執(zhí)行磺箕,那么它們應該按照它們期望的執(zhí)行順序奖慌,以同樣的順序被添加到協(xié)議處理器中。
協(xié)議攔截器的實現(xiàn)必須線程安全松靡。和servlet相似简僧,協(xié)議攔截器不應該使用實例變量,除非同步地訪問這些變量雕欺。
這是一個例子岛马,展示本地上下文如何被用來在連貫的請求之間保持一個處理狀態(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é)議處理器可以拋出兩種類型的異常:java.io.IOException
比如socket超時或重置導致的I/O失敗情況,和HttpException
比如違反HTTP協(xié)議的HTTP失敗屠列。通常I/O錯誤被認為是非致命和可恢復的啦逆,而HTTP協(xié)議錯誤被認為是致命的和不能自動回復的。請注意HttpClient
實現(xiàn)將HttpException
作為ClientProtocolException
重新拋出笛洛,而它是java.io.IOException
的一個子類夏志。這使HttpClient
的使用者可以通過一個catch語句塊,處理I/O錯誤和協(xié)議犯規(guī)苛让。
1.5.1 HTTP傳輸安全
去理解HTTP協(xié)議不適用于所有應用的類型這點是重要的沟蔑。HTTP是一個簡單的面向“請求/響應”的協(xié)議湿诊,最初被設計成支持靜態(tài)或者動態(tài)生成的內(nèi)容檢索。它從未企圖被設計成支持事務的操作瘦材。比如厅须,HTTP服務器將認為協(xié)議的部分已完成,如果它連續(xù)地接收和處理請求食棕,生成一個響應朗和,并回傳狀態(tài)碼到客戶端。如果客戶端由于讀超時宣蠕、請求取消或者系統(tǒng)崩潰例隆,在它整個生命周期中都沒有成功地接收響應甥捺,那么服務器不會去嘗試回滾事務抢蚀。如果客戶端決定重試相同的請求,服務器將會不可避免地不止一次地終止執(zhí)行同樣的事務镰禾。在一些情況下皿曲,這將導致應用數(shù)據(jù)污染,或者應用狀態(tài)的不一致吴侦。
雖然HTTP沒有被設計成支持事務的處理屋休,它仍舊可以在重要任務的應用中被作為一個傳輸協(xié)議來使用。為了確保HTTP傳輸層的安全备韧,系統(tǒng)必須確保在應用層確保HTTP方法的冪等性劫樟。
1.5.2 冪等性方法
HTTP/1.1規(guī)范是這樣定義一個冪等性方法:
[Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request]
換句話說,應用應該確保已經(jīng)有準備處理同樣方法多次執(zhí)行所產(chǎn)生的影響织堂。這可以被存檔叠艳,比如通過提供一個唯一的事務id和通過其它方式來避免相同邏輯操作的執(zhí)行。
請注意這個問題并不是HttpClient所特有的易阳「浇希基于瀏覽器的應用處理HTTP非冪等方法時,有著完全相同問題潦俺。
默認地拒课,HttpClient只假設非實體包裝的方法(比如GET和HEAD)是冪等性的,實體包裝方法(比如POST和PUT)是非冪等性的事示,由于兼容性的緣故早像。
1.5.3 自動的異常恢復
默認地肖爵,HttpClient嘗試從I/O異常中自動恢復扎酷。默認的自動恢復機制僅限于一些已知安全的異常。
- HttpClient將不會嘗試從任何邏輯的或者HTTP協(xié)議的錯誤(來自于
HttpException
類)中恢復遏匆。 - HttpClient將會自動重試那些冪等性的方法法挨。
- HttpClient將會自動重試那些傳輸異常的方法谁榜,當HTTP請求仍然正在傳輸?shù)侥繕朔掌鞯臅r候。(比如凡纳,請求還沒有被完全傳輸?shù)椒掌鳎?/li>
1.5.4 請求重試處理器
為了使一個自定義的異城灾玻恢復機制生效,應該提供一個HttpRequestRetryHandler
接口的實現(xiàn)荐糜。
HttpRequestRetryHandler myRetryHandler = new HttpRequestRetryHandler() {
public boolean retryRequest(
IOException exception,
int executionCount,
HttpContext context) {
if (executionCount >= 5) {
// Do not retry if over max retry count
return false;
}
if (exception instanceof InterruptedIOException) {
// Timeout
return false;
}
if (exception instanceof UnknownHostException) {
// Unknown host
return false;
}
if (exception instanceof ConnectTimeoutException) {
// Connection refused
return false;
}
if (exception instanceof SSLException) {
// SSL handshake exception
return false;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
if (idempotent) {
// Retry if the request is considered idempotent
return true;
}
return false;
}
};
CloseableHttpClient httpclient = HttpClients.custom()
.setRetryHandler(myRetryHandler)
.build();
請注意巷怜,