1.3 HTTP執行上下文
最初的HTTP被設計成無狀態面向請求-響應的協議,然而,現實的環境經常需要通過幾個邏輯相關聯的請求-響應交換來維持狀態信息,爲了使應用可以維持處理狀態,HttpClient允許HTTP請求在一個被稱爲HTTP上下文的特殊環境內被執行。如果一些上下文可以在連續的請求中被重用,那麼多個邏輯相關的請求就可以形成一個邏輯會話。HTTP上下文的功能和java.util.Map<String, Object>相似,它是一個任意命名值的簡單集合,一個應用可以在執行請求之前設置上下文屬性也可以在執行完成之後檢查上下文。
HttpContext可以包含任意對象,因此在多個線程之間共享的時候可能是不安全的,建議每個執行線程包含一個自己的上下文。
在執行HTTP請求的基礎上HttpClient增加了如下屬性到執行上下文:
HttpConnection實例代表到實際目標服務器的鏈接。
HttpHost實例代表鏈接目標
HttpRoute實例代表完整鏈接路由
HttpRequest實例代表實際的HTTP請求。在執行上下文中最終的HttpRequest對象總是代表發送到目標服務器的信息的狀態。默認情況下每一個HTTP1.0和HTTP1.1版本協議的請求使用相對URI,然而如果請求是通過代理並且使用非通道模式那麼就會使用絕對URI。
HttpResponse實例代表實際的HTTP響應。
java.lang.Boolean對象代表預示着實際請求是否已經被完全傳輸到鏈接目標的標誌
RequesConfig對象代表實際的請求配置。
java.util.List<URI>對象代表在請求執行過程中接受到的所有重定向路徑的集合。
也可以使用HttpClientContext適配器類簡化和上下文狀態的交互。
HttpContext context = <...> HttpClientContext clientContext = HttpClientContext.adapt(context); HttpHost target = clientContext.getTargetHost(); HttpRequest request = clientContext.getRequest(); HttpResponse response = clientContext.getResponse(); RequestConfig config = clientContext.getRequestConfig();
多請求序列代表一個邏輯相關的會話,此回話應該在相同的HttpContext實例中執行以確保交互上下文和狀態信息的在各個請求之間自動傳播。
下面的例子中,在初始的請求設置的請求配置將會被保持在執行上下文中,並且在連續的請求中得以傳播,而且會被共享在相同的上下文中。
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協議攔截器
HTTP協議攔截器是一個實現了HTTP協議具體某一方面的普通程序。通常,協議攔截器被期望對具體或者一組相關的響應消息頭操作,或者用這麼一組消息頭填充請求消息。協議攔截器也能處理內容實體,封裝了透明消息內容實體的壓縮/解壓就是一個很好的例子,通常這些處理通過裝飾者模式完成,在裝飾者模式中,一個封裝實體類用來裝飾原始實體。另外,幾個協議攔截器合併在一起可以構成一個邏輯單元。
協議攔截器可以通過共享信息工作,例如通過HTTP執行上下文共享處理狀態。協議攔截器可以使用HTTP上下文爲一個或者幾個連續的請求存儲處理狀態。
一般情況下,只要不依賴一個特殊的執行上下文狀態,攔截器的執行順序都是沒關係的。如果協議攔截器是相互依賴的並且因此需按照一個特殊的順序執行,那麼他們應該按照一定的順序被添加到協議處理器中。
協議攔截器必須以線程安全的方式被實現。這點兒類似servlet,協議攔截器不應該使用實例變量除非訪問這些變量是線程同步的。
下面是一個本地上下文如何在連續的請求之間被用來持續保留處理狀態的例子:
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.1 HTTP傳輸安全
理解HTTP協議並不適合所有類型的應用是重要的。HTTP只是簡單的面向請求響應的協議,最初它被設計成支持靜態或動態生成內容檢索,它從來沒有被考慮過用來支持事務操作,例如,作爲HTTP服務器如果接受、處理請求、生成響應和發送狀態碼到客戶端成功但是客戶端因爲讀取超時、請求取消或者系統宕機而導致接受響應失敗,那麼服務器不會嘗試回滾事務。如果客戶端重試相同的請求,那麼服務器不再執行相同的事務。有時這會導致應用數據污染或者不一致的應用狀態。
儘管HTTP從來沒有被考慮過支持事務處理,但它仍然作爲關鍵應用的傳輸協議,爲了確保HTTP傳輸層的安全系統必須確保在應用層的HTTP方法的冪等性。
1.5.2 冪等方法
HTTP/1.1 指導說明這樣定義一個冪等方法:
【方法也可以有冪等性,其冪等性(忽略錯誤和過期問題)表現在N(N>0)次相同的請求所帶來的影響和一次請求帶來的影響相同】
換句話說,應用程序應該確保準備好應對同一方法被多次執行所帶來的影響。其實這可以解決,例如通過提供一個唯一的事務ID,或者通過其他方法避免相同邏輯操作的執行。
請注意,這一問題並不是HttpClient特有的,基於瀏覽器的應用當涉及到HTTP方法的非冪等性時也有相同的問題。
默認情況下,HttpClient僅保證形如GET、HEAD等非實體包含方法的冪等性,這也是形如POST、PUT等實體包含方法不兼容的原因。
1.5.3 自動異常恢復
默認情況下,HttpClient嘗試自動從I/O異常中恢復,默認的自動恢復功能只限制於幾個安全的異常。
HttpClient不會從任何邏輯的或者HTTP協議錯誤(繼承於HttpException類的異常)中恢復。 HttpClient會自動重試冪等性的方法。
HttpClient會重試那些因傳輸異常而失敗但是HTTP請求仍然正在被傳輸至目標服務器的方法(例如,請求還沒有完全被傳輸至服務器)。
1.5.4 請求重試處理器
爲了確保一個定製的異常恢復機制,應該提供一個HttpRequestRetryHandler接口的實現。
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();
請注意,可使用StandardHttpRequestRetryHandler代替默認的用於把這些被RFC-2616定義爲冪等的請求方法當做自動嘗試安全的方法,這些方法包括:GET,HEAD,PUT,DELETE,OPTIONS和TRACE。
1.6 終止請求
在某些情況下由於目標服務器負載過高或者客戶端出現太多併發請求而導致HTTP請求執行在預期時間內沒有完成,在這種情況下,儘早終止請求並且清除阻塞在I/O操作上的線程的執行是必要的。被HttpClient執行的HTTP請求可以通過調用HttpUriRequest的abort()方法在執行的任何階段被終止。此方法是線程安全的並且可以被任何線程調用。當一個HTTP請求被終止了他的線程,即使該線程正阻塞在I/O操作上也能通過拋出InterruptedIOException異常的方式保證線程被終止。
1.7 重定向處理
HttpClient自動處理所有類型的重定向,除了那些因需用戶干預而明確被HTTP規範禁止的重定向,參見其他(狀態碼爲303)重定向,如POST和PUT的請求被轉換成GET請求因HTTP規範要求禁止重定向。可以使用一個定製的重定向策略來鬆懈被HTTP規範強制限制的POST方法的自動重定向。
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient =HttpClients.custom().
setRedirectStrategy(redirectStrategy).build();
HttpClient不得不經常在執行過程中重寫請求信息,每一個默認的HTTP/1.0和HTTP/1.1版本的請求通常使用相對URI,同樣地,原始請求可能會從一個地址多次重定向到另一個地址。最終的絕對HTTP地址可以通過使用原始請求和上下文構建。工具方法URIUtils的resolve()方法可以被用來構建用來生成最終請求的絕對URI。此方法包含來自重定向請求或者原始請求的最後定位符片段。
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()); // Expected to be an absolute URI } finally { response.close(); }