1.2 HttpClient 接口
HttpClient
接口代表著HTTP請求執(zhí)行的最基本的契約翩迈。它對請求的執(zhí)行沒有什么限制與特定的細(xì)節(jié)澈灼,并且對于連接管理湿蛔、狀態(tài)管理创译、驗(yàn)證和重定向的處理抵知,是讓各自的實(shí)現(xiàn)來決定。這讓它更容易對接口進(jìn)行裝飾软族,添加額外的功能刷喜,如對響應(yīng)內(nèi)容進(jìn)行緩存。
通常立砸,HttpClient
實(shí)現(xiàn)是當(dāng)作一個門面掖疮,可以包含一系列特殊目的的處理器或者策略接口,如重定向或驗(yàn)證處理颗祝,或決定連接持久性和弊巧粒活時長的策略接口。這使用戶能夠選擇性地替換對應(yīng)的默認(rèn)策略螺戳。
@Test
public void chapter1_2_0() {
ConnectionKeepAliveStrategy keepAliveStrategy = 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(keepAliveStrategy)
.build();
}
1.2.1 HttpClient 線程安全
HttpClient
實(shí)現(xiàn)是線程安全的搁宾。對于多個請求的執(zhí)行,推薦使用同一個實(shí)例倔幼。
1.2.2 HttpClient 資源釋放
當(dāng)一個CloseableHttpClient
實(shí)例不再需要并且將超出連接管理器的作用范圍時盖腿,它必須調(diào)用方法CloseableHttpClient#close()
來關(guān)閉。
public void chapter1_2_2() throws IOException {
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
// ...
} finally {
httpclient.close();
}
}
1.3 HTTP 執(zhí)行上下文
最初HTTP被設(shè)計成一個無狀態(tài)的损同,面向請求-響應(yīng)的協(xié)議翩腐。然而,真實(shí)世界的應(yīng)用程序經(jīng)常需要在幾個邏輯關(guān)聯(lián)的請求響應(yīng)交換中保持狀態(tài)信息膏燃。為了能夠讓應(yīng)用程序能夠維持狀態(tài)栗菜,HttpClient允許Http請求在一個特定的執(zhí)行上下文中執(zhí)行,該上下文被稱為HTTP上下文(HTTP context)蹄梢。當(dāng)相同的上下文在連續(xù)的請求中重用時,幾個邏輯關(guān)聯(lián)的請求就會參與一個邏輯的會話。HTTP上下文類似于java.util.Map<String, Object>
禁炒。它就是一個簡單鍵值對的集合而咆。應(yīng)用程序能夠在執(zhí)行前獲取上下文,與能夠在執(zhí)行后檢查上下文幕袱。
HttpContext
能夠包含任意對象暴备,所以在多個線程中共享可能是不安全的。推薦每個線程維護(hù)自己的上下文们豌。
在HTTP請求的執(zhí)行過程中涯捻,HttpClient會添加以下屬性到掛靠上下文中:
-
HttpConnection
實(shí)例,表示與目標(biāo)服務(wù)器的實(shí)際連接望迎。 -
HttpHost
實(shí)例障癌,表示連接的目標(biāo)。 -
HttpRoute
實(shí)例辩尊,表示完整的連接路由涛浙。 -
HttpRequest
實(shí)例,表示實(shí)際的HTTP請求摄欲。由final修飾的HttpRequest對象總是能正確表示報文的狀態(tài)轿亮,因?yàn)樗潜凰屯繕?biāo)服務(wù)器。對于缺省的HTTP/1.0和HTTP/1.1胸墙,它使用相對路徑的請求URI我注。然而,如果請求是通過一個非隧道模式的代理來發(fā)送的話迟隅,URI是絕對路徑的但骨。 -
HttpResponse
實(shí)例,表示實(shí)際的HTTP響應(yīng)玻淑。 -
java.lang.Boolean
對象嗽冒,表示的標(biāo)志,指明實(shí)際的請求是否被完全傳送到連接目標(biāo)补履。 -
ReqeustConfig
對象添坊,表示實(shí)際請求的配置。 -
java.util.List<URI>
對象箫锤,表示在執(zhí)行過程中收到的所有的重定向位置的集合贬蛙。
我們可以使用HttpClientContext
適配器來簡化與上下文狀態(tài)的交互過程。
public void chapter1_3() {
HttpContext context = null; // init ...
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig requestConfig = clientContext.getRequestConfig();
}
一組邏輯關(guān)聯(lián)的請求應(yīng)當(dāng)在相同的HttpContext
實(shí)例中執(zhí)行谚攒,以保證在多個請求中自動傳遞會話上下文和狀態(tài)信息阳准。
在下面的例子中,由初始請求設(shè)置的請求配置會保存在執(zhí)行上下文中馏臭,并通過共享相同的上下文來傳遞給接下來的請求野蝇。
public void chapter1_3_a() throws IOException {
CloseableHttpClient httpclient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(1000)
.setConnectTimeout(1000)
.build();
HttpContext context = new BasicHttpContext();
HttpGet httpGet1 = new HttpGet("http://www.baidu.com");
httpGet1.setConfig(requestConfig);
CloseableHttpResponse response1 = httpclient.execute(httpGet1, context);
try {
HttpEntity entity1 = response1.getEntity();
} finally {
response1.close();
}
HttpGet httpGet2 = new HttpGet("http://www.yy.com");
CloseableHttpResponse response2 = httpclient.execute(httpGet2, context);
try {
HttpEntity entity2 = response2.getEntity();
} finally {
response2.close();
}
}
1.4 HTTP 協(xié)議攔截器
HTTP協(xié)議攔截器是一個實(shí)現(xiàn)了HTTP協(xié)議一個特定方面的例程。通常,協(xié)議攔截器期望作用于輸入報文的一個特定的頭部或一組相關(guān)的頭部绕沈,或給輸出報文填入一個特定的頭部或一組相關(guān)的頭部锐想。協(xié)議攔截器也能夠操作報文中的內(nèi)容實(shí)體,透明的內(nèi)容壓縮/解壓縮就是一個很好的例子乍狐。通常使用裝飾者模式來實(shí)現(xiàn)赠摇,用包裝的實(shí)體來裝飾原始的實(shí)體。多個協(xié)議攔截器能夠組合起來形成一個邏輯單元浅蚪。
協(xié)議攔截器在HTTP執(zhí)行上下文中通過共享信息來協(xié)作藕帜,如處理狀態(tài)。協(xié)議攔截器能夠使用HTTP上下文來保存一個請求或幾個連續(xù)請求的處理狀態(tài)惜傲。
通常攔截器的順序應(yīng)該是無關(guān)緊要的洽故,只要它們不依賴執(zhí)行上下文中的特定的狀態(tài)。如果協(xié)議接口器存在依賴必須按一定的順序執(zhí)行操漠,它們應(yīng)該按照該順序添加到協(xié)議處理器中收津。
協(xié)議接口器必須實(shí)現(xiàn)為線程安全的。與servlet類似浊伙,協(xié)議攔截器不應(yīng)該使用實(shí)例變量撞秋,除非訪問那些實(shí)例變量是同步的。
以下的例子展示的本地上下文在連續(xù)的請求中是如何被使用來保存處理狀態(tài)的:
public void chapter1_4() throws IOException {
CloseableHttpClient httpclient = HttpClients.custom()
.addInterceptorLast(new HttpRequestInterceptor() {
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
AtomicInteger count = (AtomicInteger) httpContext.getAttribute("count");
httpRequest.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
(I/O錯誤如socket超時或者socket重置)和HttpException
(表示HTTP錯誤如違反HTTP協(xié)議)嚣鄙。 通常I/O錯誤可以認(rèn)為是非致命的和可恢復(fù)的吻贿,而HTTP協(xié)議錯誤是致命的且不能自動恢復(fù)的。請注意HttpClient
的實(shí)現(xiàn)把HttpException
重新拋出為ClientProtocolException
哑子,它是java.io.IOException
的子類舅列。這使得用戶可以在一個catch子句中處理I/O錯誤和協(xié)議錯誤。
1.5.1 HTTP 傳輸安全
理解HTTP協(xié)議不是對所有應(yīng)用程序都適用是很重要的卧蜓。HTTP是一個簡單的面向請求/響應(yīng)的協(xié)議帐要,它最初被設(shè)計成支持靜態(tài)或動態(tài)產(chǎn)生的內(nèi)容的獲取。它從未打算支持事務(wù)操作弥奸。例如榨惠,HTTP服務(wù)器成功地接收并處理請求,產(chǎn)生一個響應(yīng)并發(fā)送狀態(tài)碼給回客戶端盛霎,那么服務(wù)器就會認(rèn)為它成功了赠橙。如果客戶端因?yàn)樽x超時、取消請求或系統(tǒng)崩潰愤炸,服務(wù)器不會試圖回滾這個事務(wù)期揪。如果客戶端決定進(jìn)行重試相同的請求,那么服務(wù)器不可避免地會重復(fù)執(zhí)行一次相同的請求规个。在某種情況下凤薛,這會導(dǎo)致數(shù)據(jù)損壞或程序狀態(tài)不一致姓建。
盡管HTTP從未被設(shè)計成支持事務(wù)處理,它仍然能夠被用作關(guān)鍵任務(wù)的傳輸協(xié)議枉侧,如果特定的條件滿足的話引瀑。為了保證HTTP傳輸層安全,系統(tǒng)必須保證在應(yī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 os N > 0 identical requests is the same as for a single request.
也就是說,應(yīng)用程序應(yīng)當(dāng)確保它能夠多次執(zhí)行相同方法而不產(chǎn)生副作用帜矾。提供一個唯一的事務(wù)ID或用其他方法避免執(zhí)行相同邏輯操作就能達(dá)到這一目的翼虫。
請注意到這不僅限于HttpClient÷庞基于瀏覽器的應(yīng)用程序一樣有相同的問題珍剑。
HttpClient默認(rèn)認(rèn)為只有非包含實(shí)體的方法,如GET
和HEAD
是冪等的死陆,包含實(shí)體的方法如POST
和PUT
招拙,基于兼容性原因,是非冪等的措译。
1.5.3 自動的異潮鸱铮恢復(fù)
HttpClient默認(rèn)會自動地從I/O異常中恢復(fù)。缺省的自動恢復(fù)機(jī)制僅適用于一些已知是安全的異常领虹。
- HttpClient不會試圖從任務(wù)邏輯錯誤或HTTP協(xié)議錯誤(那些繼續(xù)
HttpException
的類)中自動恢復(fù)规哪。 - HttpClient會自動重試那些認(rèn)為是冪等的方法。
- HttpClient 會自動重試那些有傳輸異常的方法塌衰,如果該HTTP請求仍然在傳輸給目標(biāo)服務(wù)器(例如沒有完全傳輸?shù)椒?wù)的請求)诉稍。
1.5.4 請求重試處理器
為了實(shí)現(xiàn)自定義的恢復(fù)機(jī)制,我們可以提供一個HttprequestRetryHandler
接口的實(shí)現(xiàn)最疆。
public void chapter1_5_4() {
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 ConnectTimeoutException) {
// Connection refused
return false;
}
if (exception instanceof InterruptedIOException) {
// Timeout
return false;
}
if (exception instanceof UnknownHostException) {
// Unknown host
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();
}
請注意到我們可以使用StandardHttpRequestRetryHandler
杯巨,而不是使用默認(rèn)的處理器,來自動重試那些被RFC-2616
定義為冪等的那些請求方法:GET
, HEAD
, PUT
, DELETE
, OPTIONS
和TRACE
努酸。
1.6 中止請求
在某些情況下服爷,HTTP請求在期望的時間內(nèi)未執(zhí)行成功,可能是因?yàn)槟繕?biāo)服務(wù)器的高負(fù)載或在客戶端有過多的并發(fā)請求蚊逢。這種情況下层扶,可能需要過早的結(jié)束請求并釋放被I/O操作阻塞的線程。任何被HttpClient執(zhí)行的HTTP請求能夠在任何階段通過調(diào)用HttpUriRequest#abort()
方法來中止烙荷。這個方法是線程安全的且能被任何線程調(diào)用镜会。當(dāng)一個HTTP請求被中止,它的執(zhí)行線程(即使被I/O操作阻塞)也能夠保證解鎖(拋出異常InterruptedIOException
)终抽。
1.7 重定向處理
HttpClient自動處理所有類型的重定向戳表,除了那些被HTTP規(guī)約顯示禁止的需要用戶干預(yù)的桶至。參考其他在POST
和PUT
上的重定向(狀態(tài)碼303),HTTP規(guī)約要求其轉(zhuǎn)換成GET
請求匾旭。我們可以使用一個自定義的重定向策略來放寬HTTP規(guī)約對POST方法的限制镣屹。
public void chapter1_7_a() {
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
}
HttpClient經(jīng)常在執(zhí)行的過程中重寫請求報文。對于默認(rèn)的HTTP/1.0和HTTP/1.1來說通常是使用相對路徑的請求URI价涝。同樣地女蜈,原始請求可能被多次重定向到另一個位置上。最終被解析的絕對路徑的HTTP地址色瘩,能夠使用原始請求和上下文來構(gòu)建伪窖。工具方法URIUtils#resolve
就是用來構(gòu)建最終解析到的絕對路徑URI,從而生成最終的請求居兆。這個方法包含重定向請求中的最后一個段的標(biāo)識或原始請求覆山。
@Test
public void chapter1_7_b() throws IOException, URISyntaxException {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpGet = new HttpGet("http://www.yy.com");
CloseableHttpResponse response = httpclient.execute(httpGet, context);
try {
HttpHost target = context.getTargetHost();
List<URI> redirectLocation = context.getRedirectLocations();
URI location = URIUtils.resolve(httpGet.getURI(), target, redirectLocation);
System.out.println("Final Http location: " + location.toASCIIString());
} finally {
response.close();
}
}