HttpClient4.5教程-第一章-基礎1.2-1.8

子沭發表於2016-03-08

1.2 HttpClient 介面

HttpClient介面代表HTTP request執行的最基本的協議。它不規定任何request執行過程中的限制或者細節,並且將連線管理,狀態管理,認證和重定向處理交給各自獨立實現,這使得它更容易去使用附加功能去裝飾介面,如response content cacheing。

通常HttpClient的實現作為一系列特定的handler或者strategy介面的入口,而這些特定的handler或者strategy各自負責HTTP協議處理的一個方面,比如重定向或者認證處理,或者連線持久化和保活週期處理。這使得使用者可以有選擇的替代這些方面預設實現。

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的實現是執行緒安全的,建議不同的request 執行過程用同一個該類的例項。

1.2.2 HttpClient資源釋放

當CloseableHttpClient不再使用並且將離開其所關聯的連線管理器的範圍時,其必須通過呼叫CloseableHttpClient#close方法關閉。

CloseableHttpClient httpclient = HttpClients.createDefault();
try {
    <...>
} finally {
    httpclient.close();
}

1.3 HTTP執行期上下文

最初HTTP被設計為無狀態的,面向response-request的協議。但是現實世界中應用程式經常需要通過一些邏輯相關的request-response交換來保持狀態資訊。為了讓應用程式能夠保持處理狀態,HttpClient允許HTTP requests在一個特別的執行期上下文中執行,即HTTP context。多數連續的邏輯相關的request如果使用相同的context,那麼他們可以分享同一個邏輯session,HTTP context有點像java.util.Map<String,Object>,它只是簡單的手機任意形式的name-value。應用程式可以在request執行之前填充context或者市在執行之後檢查context。

HttpContext可以包含任意的物件,因此在不同的執行緒之間共享是不安全的,建議每個執行緒儲存其自己的Context。

在HTTP request請求的過程中,HttpClient將會新增如下屬性到執行上下文中。

HttpConnection 例項代表著到目標伺服器的真實連線。

HttpHost例項代表著連線的目標

HttpRoute例項代表著完整的連線路由

HttpRequest 例項代表著實際的HTTP request請求,因為HttpRequest是傳送到目標伺服器中的,所以它最終在執行上下文中的狀態即表示著這條訊息的狀態。一般來說HTTP/1.0和HTTP、1.1的request都預設使用相對路徑,但是如果request是通過代理伺服器,而且工作在非隧道模式下,那麼這時就必須使用絕對路徑了。

HttpResponse 例項代表著實際的HTTP response

java.lang.Boolean 物件標識著request是否完全傳送到了目標伺服器

RequestConfig 物件表示實際的request配置

java.util.List<URI> 物件表示著在執行request過程中受到的所有重定向請求。

你可以使用HttpClientContext介面卡類簡化與context的互動操作。

HttpContext context = <...>
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();

多個邏輯相關,依賴於session的request應該在同樣的HttpContext上下文環境中執行以保證會話上下文和狀態能夠在不同的request中傳遞。

在下面的例子中,request初始化時建立了request configuration配置,此配置會儲存在執行上下文中並且在連續不同的request之間傳遞用以分享同樣的上下文。

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 protocol 攔截器

HTTP protocol攔截器是實現了HTTP protocol某些特定方面的一段程式。通常,protocol攔截器會對入棧訊息的某一個或者一組指定的headers起作用,或者用某一個或者一組特定的headers填充出棧訊息。protocol攔截器也可以操作封裝好內容實體的訊息,比如進行內容壓縮或者解壓縮,通常我們會通過‘Decorator’模式來完成這些操作,特別是在使用實體包裝器類來封裝源實體時。多個protocol攔截器可以組合形成一個邏輯性的單元。

Protocol攔截器可以通過共享資訊(比如HTTP執行上下文中的處理狀態)來進行合作,Protocol攔截器也可以通過HTTPContext來儲存一個或者一系列request的處理狀態。

由於攔截器不依賴於執行環境中的某個特定的狀態,所以他們的執行順序並不重要,但是如果你對protocol的執行順序有要求,那麼他們應該按照執行的順序新增到protocol processor中去。

Procotol必須是執行緒安全的,跟Servlets類似,protocol攔截器不應該使用例項變數,除非這些例項變數是同步的。

下面是一個local context在連續的request中儲存處理狀態的例子。

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. Exception 處理

HTTP protocol processors 會丟擲兩種異常:

java.io.IOException 在I/O失敗時,如socket超時或者socket重置。

HttpException  標識HTTP 失敗,如違反HTTP協議。

通常 I/O 失敗是非致命並且可以恢復的,而HTTP protocol錯誤是致命的並且不可自動恢復的。請注意HttpClient的實現類將ClientProtocolException當做HttpException丟擲,ClientProtocolException是java.io.IOException的子類,這使得使用者在一個catch裡可以同時處理I/O錯誤和協議錯誤。

1.5.1 HTTP 安全傳輸

要明白HTTP 協議並不是適合所有的應用。HTTP 是一個簡單的request/response協議,它在最初的設計用於靜態/動態內容檢索,它從來沒有試圖去支援事務操作。舉個例子來說,HTTP 伺服器在接收並且處理request時只會考慮完成自己負責的這一部分協議的工作,生成response和傳送狀態碼給客戶端,如果客戶端接收失敗(讀取超時,請求取消或者系統崩潰),伺服器不會嘗試去回滾這個操作,如果伺服器支援事務,那麼當客戶端決定重試請求,伺服器將不止一次的強制終止執行相同的事務,而這樣會導致應用的資料損壞或者狀態不一致。

即使HTTP從來沒有考慮過支援事務處理,在某些特定的情況下它也可以作為關鍵應用的傳輸協議,為了保證HTTP傳輸層的安全,使用系統必須在應用層確保HTTP 方法的冪等性。

1.5.2 方法冪等性

HTTP/1.1 規範將冪等性做如下定義:

[ 方法有冪等性即表示N>0個相同請求的副作用跟單一請求的副作用一樣 ]

換句話來說就表示應用應該確保已準備好處理執行多個相同方法帶來的影響,而這是有辦法可以實現的,比如提供一個唯一的事務id,意味著避免執行相同的邏輯操作。

請注意這個問題並不是專門針對於HttpClient,瀏覽器都會受到相同問題的影響。

基於相容性的原因,HttpClient假設只有非實體封裝的方法,比如GET 和HEAD是冪等性的,而實體封裝方法如POST和PUT不是冪等性的。

1.5.3 異常自動恢復

HttpClient預設會自動嘗試從I/O異常中恢復,自動恢復機制只作用於一些已經被認為是安全的異常:

HttpClient不會嘗試從任何邏輯或者HTTP protocol錯誤中恢復(HttpException的衍生類)。

HttpClient會自動重試那些被認為是冪等性的方法。

HttpClient會自動重試在Http request傳送到目標伺服器時出現傳輸失敗的方法。

1.5.4 Request retry handler

為了啟用自定義的錯誤恢復機制,你應該提供一個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,AND TRACE)。

1.6 終止請求

在某些情況下,HTTP request在期望的時間內無法執行完成,導致目標伺服器的高負載或者客戶端存在許多同步的請求,從而你會需要去提前中斷request請求並且將阻塞在I/O操作的執行執行緒釋放出來,在HttpClient中執行的request可以在執行過程中的任何階段通過呼叫HttpUriRequest#abort()終止,這個方法是執行緒安全的並且可以從任何執行緒起調。當HTTP request在執行執行緒中終止時,會丟擲InterruptedIOException。

1.7 Redirect Handling

HttpClient自動處理所有型別的重定向,除了在HTTP規範中明確禁止並且要求使用者干預的。在HTTP規範中,通過POST和PUT請求的303 redirect會被轉換成GET請求。你可以使用自定義重定向策略去放寬HTTP規範中關於POST方法重定向的限制。

LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
        .setRedirectStrategy(redirectStrategy) 
        .build();

 

HttpClient在其執行過程中經常需要重寫request訊息。HTTP/1.0和HTTP/1.1經常使用相對請求,同樣的,原始請求可能會被重定向多次,最終確定的HTTP路徑可以通過原始request和context組合出來,URIUtils#resolve實用方法可以用來明確的最終請求的URI路徑。這個方法包括重定向請求或者原始request的最後一個片段識別符號。

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();
}


相關文章