HttpClient-4.5.2官方教程完整翻譯

HttpClient 4.5.2

前言

超文本傳輸協議(HTTP)可能是當今互聯網上使用的最重要的協議。
網絡服務,支持網絡的設備以及網絡計算的發展繼續擴大了HTTP協議在用戶驅動的Web瀏覽器之外的作用,同時增加了需要HTTP支持的應用程序的數量。

儘管java.net包提供了通過HTTP訪問資源的基本功能,但它並不能提供許多應用程序所需的全部靈活性或功能。
HttpClient試圖通過提供一個高效的,最新的,功能豐富的包來實現最新的HTTP標準和建議的客戶端來填補這個空白。

爲擴展而設計,同時爲基本的HTTP協議提供強大的支持,任何構建HTTP感知的客戶端應用程序(例如Web瀏覽器,Web服務客戶端或利用或擴展HTTP協議進行分佈式通
信的系統)的HttpClient都可能是有用的。

HttpClient scope

  • 基於HttpCore的客戶端HTTP傳輸庫
  • 基於經典(阻塞)I / O
  • 內容不可知的

HttpClient 不是什麼

HttpClient不是一個瀏覽器。這是一個客戶端HTTP傳輸庫。HttpClient的目的是發送和接收HTTP消息。HttpClient不會嘗試處理內容,執行嵌入HTML頁面的
JavaScript,嘗試猜測內容類型(如果沒有明確設置),或者重新格式化請求/重寫位置URI或與HTTP傳輸無關的其他功能。

基礎

請求的執行

HttpClient最重要的功能是執行HTTP方法。執行HTTP方法涉及一個或多個HTTP請求/ HTTP響應交換,通常由HttpClient內部處理。
用戶需要提供一個用於執行的請求對象,HttpClient需要向目標服務器發送請求並得到相應的響應對象,如果執行不成功則拋出異常。
自然地,定義以上內容的接口就是HttpClient API的主要入口點。
下面是最簡單的請求執行過程示例:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    <...>
} finally {
    response.close();
}

HTTP請求

所有HTTP請求都有一個請求行,它包含一個方法名,一個請求URI和一個HTTP協議版本。

HttpClient支持HTTP / 1.1規範中定義的所有HTTP方法:GET,HEAD,POST,PUT,DELETE,TRACE和OPTIONS。每個方法類型都有一個特定的類:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。

Request-URI是一個統一資源標識符,用於標識應用請求的資源。HTTP請求URI由協議方案,主機名,可選的端口,資源路徑,可選的查詢和可選的片段組成。

HttpGet httpGet = new HttpGet("http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
  •  

HttpClient提供URIBuilder實用程序類來簡化請求URI的創建和修改。

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=

HTTP響應

HTTP響應是服務器收到請求消息後執行相關操作後發送回客戶端的消息。該消息的第一行由協議版本和數字狀態碼及其相關的文本短語組成。

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

使用 Message Header

HTTP消息可以包含許多描述消息屬性的header,如內容長度,內容類型等等。HttpClient提供了檢索,添加,刪除和枚舉header的方法。

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

獲取給定類型的headers的最有效的方法是使用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消息解析爲單獨的header元素。
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

HTTP Entity

HTTP消息可以攜帶與請求或響應相關聯的內容實體。實體只可以在一些請求和一些響應中找到,因爲它們是可選的。使用實體的請求被稱爲實體封閉請求(entity enclosing requests)。HTTP規範定義了兩個實體封閉請求方法:POST和PUT。響應通常會攜帶消息體。此規則也有例外情況,例如對HEAD方法的響應和對204無內容、304未修改、205重置內容的響應。

  • streamed: 內容是從一個流接收的,或者是隨時產生的。具體來說,這個類別包括從HTTP響應中收到的實體。流派實體通常不可重複。
  • self-contained: 內容在內存中或通過獨立於連接或其他實體的方式獲得。該類實體通常可重複。這種類型的實體將主要用於包含HTTP請求的實體。
  • wrapping: 內容是從另一個Entity獲得的。

當使用流從響應中讀取內容時,這種區別對於連接管理非常重要。對於由應用程序創建並僅使用HttpClient發送的請求實體,流式和自包含之間的區別並不重要。在這種情況下,建議將不可重複的實體視爲流式,將可重複的實體視爲獨立式。

可重複的Entity

一個實體可以是可重複的,這意味着它的內容可以被多次讀取。這隻適用於自包含的實體(如ByteArrayEntity或StringEntity)

使用 Http Entities

由於實體可以包含二進制和字符內容,因此它支持字符編碼(以支持後者,即字符內容)。

在執行帶有封閉內容的請求時或者請求成功後使用響應主體將結果發送回客戶端時創建實體。

要讀取entity內容可以使用HttpEntity的getContent() 方法,該方法返回一個 java.io.InputStream,或者提供一個輸出流給HttpEntity的writeTo(OutputStream)方法,該方法會將entity的內容寫入到給定的輸出流。

當收到一個傳入消息的實體時,HttpEntity的getContentType()方法和getContentLength()方法可以用來讀取元數據,例如Content-Type 和 Content-Length headers(這些數據如果已經被提供)。由於Content-Type頭可以包含文本MIME類型(如text / plain或text / html)的字符編碼,因此使用HttpEntity#getContentEncoding()方法來讀取此編碼。如果header不可用,則將返回長度爲-1,內容則返回NULL。如果Content-Type頭部可用,則會返回一個Header對象。

爲發送消息創建實體時,此元數據必須由實體的創建者提供。

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
  • 確保釋放低級資源

爲了確保正確釋放系統資源,必須關閉與實體相關的內容流或響應本身。

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

關閉內容流和關閉響應的區別在於,前者將嘗試通過消耗實體內容來保持底層連接的活動,而後者立即關閉並放棄連接。

請注意HttpEntity#writeTo(OutputStream)方法也需要確保一旦實體被完全寫出,正確釋放系統資源。如果此方法通過調用HttpEntity#getContent()來獲取java.io.InputStream的實例,則還應該在finally子句中關閉該流。

在使用流式實體時,可以使用EntityUtils#consume(HttpEntity)方法確保實體內容已被完全消耗,並且基礎流已關閉。

然而,可能有這樣的情況,當只需要檢索整個響應內容的一小部分,並且消耗剩餘內容或使得連接可重用的性能損失太高時,在這種情況下,可以通過關閉響應來終止內容流。連接不會被重用,它所擁有的所有關卡資源將被正確釋放。

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

消費消息體

推薦使用實體內容的方法是使用HttpEntity#getContent()或HttpEntity#writeTo(OutputStream)方法。

HttpClient也附帶了EntityUtils類,它公開了幾個靜態方法來更容易地從一個實體讀取內容或信息。

通過這些方法可以使用String或byte取回所有消息體內容而不是直接使用io流。
但是,強烈建議不要使用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();
}

在某些情況下,可能需要不止一次地閱讀實體內容。在這種情況下,實體內容必須以某種方式緩衝,無論是在內存中還是在磁盤上。最簡單的方法是使用BufferedHttpEntity類包裝原始實體。這將導致原始實體的內容被讀入內存緩衝區。在其他方面與原始實體相同。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
    entity = new BufferedHttpEntity(entity);
}

創建消息體

HttpClient提供了幾個類,可以通過HTTP連接有效地流式輸出內容。這些類的實例可以與實體封閉請求(如POST和PUT)相關聯,可以將實體內容包含在傳出的HTTP請求中。HttpClient爲大多數常用數據容器(如字符串,字節數組,輸入流和文件)提供了幾個類: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不可重複,因爲它只能從底層數據流中讀取一次。通常建議實現一個自定義的HttpEntity類,它是自包含的,而不是使用通用的InputStreamEntity。 FileEntity可以是一個很好的起點。

HTML 表單

例如,許多應用程序需要模擬提交HTML表單的過程,以便登錄到Web應用程序或提交輸入數據。

HttpClient提供了實體類UrlEncodedFormEntity來便捷實現過程。

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編碼來對參數進行編碼並生成以下內容:
param1=value1&param2=value2

內容分塊(content chunking)

通常建議讓HttpClient根據正在傳輸的HTTP消息的屬性選擇最合適的傳輸編碼。
但是,可以通過將HttpEntity#setChunked()設置爲true來通知HttpClient塊編碼。
請注意,HttpClient將僅使用此標誌作爲提示。
使用不支持塊編碼的HTTP協議版本(例如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);
  • 處理響應

處理響應最簡單最方便的方法是使用ResponseHandler接口,該接口包含handleResponse(HttpResponse響應)方法。
這個方法完全緩解了用戶對於連接管理的擔心。
當使用ResponseHandler時,無論請求執行成功還是導致異常,HttpClient都會自動確保連接釋放回連接管理器。

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

HttpClient 接口

HttpClient接口表示了HTTP請求執行最重要的協議。它沒有對請求執行過程施加任何限制或特定的細節,而是將連接管理,狀態管理,認證和重定向處理的細節留給個人實現。這將更容易擴展接口,例如添加響應內容緩存功能。

一般來說,HttpClient實現是作爲一個特殊用途處理程序的外觀或負責處理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();
  • HttpClient 線程安全

HttpClient實現是線程安全的。建議將此類的同一個實例重用於多個請求執行。

HttpClient資源釋放

當不再需要實例CloseableHttpClient並且即將超出範圍時,必須通過調用CloseableHttpClient#close()方法關閉與其關聯的連接管理器。

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

HTTP 執行上下文

最初,HTTP被設計爲一種無狀態,面向響應請求的協議。但是,真實世界的應用程序通常需要能夠通過幾個邏輯相關的請求 - 響應交換來保存狀態信息。爲了使應用程序保持處理狀態,HttpClient允許HTTP請求在特定的執行上下文(被稱爲HTTP上下文)內執行。如果在連續的請求之間重複使用相同的上下文,多個邏輯相關的請求可以參與邏輯會話。HTTP上下文的功能類似於java.util.Map

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

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

異常處理

HTTP協議處理器可能會拋出兩種類型的異常:在發生I / O故障(如套接字超時或套接字重置)時發生java.io.IOException異常,以及發出HTTP異常(如違反HTTP協議)的HttpException。通常I / O錯誤被認爲是非致命的,可恢復的,而HTTP協議錯誤被認爲是致命的,不能自動從中恢復。請注意,HttpClient實現將HttpExceptions重新拋出爲ClientProtocolException,它是java.io.IOException的子類。這使得HttpClient的用戶可以從單個catch子句處理I / O錯誤和協議違規。

HTTP傳輸安全

HTTP協議並不適用於所有類型的應用程序,這一點很重要。HTTP是一種簡單的面向請求/響應的協議,最初設計用來支持靜態或動態生成的內容檢索。從來沒有打算支持交易操作。例如,如果HTTP服務器成功地接收並處理請求,產生響應並將狀態碼發送回客戶端,則HTTP服務器將認爲其合同的一部分被執行。如果客戶端由於讀取超時,請求取消或系統崩潰而未能全部收到響應,服務器將不會嘗試回滾事務。如果客戶決定重試相同的請求,服務器將不可避免地不止一次地執行相同的事務。在某些情況下,這可能會導致應用程序數據損壞或應用程序狀態不一致。

儘管HTTP從未被設計爲支持事務處理,但是在滿足特定條件的情況下,它仍然可以用作關鍵應用程序的傳輸協議。爲了確保HTTP傳輸層的安全,系統必須確保應用層HTTP方法的冪等性。

冪等方法

HTTP / 1.1規範定義了一個冪等方法 [方法也可以具有“冪等性”的性質(除了錯誤或期滿問題)N> 0相同請求的副作用與單個請求相同]
換句話說,應用程序應該確保它準備好處理同一方法的多次執行的影響。這可以通過例如提供唯一的事務ID以及通過避免執行相同的邏輯操作的其他方式來實現。

請注意,這個問題不是特定於HttpClient的。基於瀏覽器的應用程序受到與HTTP方法非冪等性相同的問題的影響。

默認情況下,HttpClient假定只有非實體封閉的方法(如GET和HEAD)是冪等的,實體封裝方法(如POST和PUT)不是出於兼容性的原因。

自動異常恢復

默認情況下,HttpClient會嘗試從I / O異常中自動恢復。默認的自動恢復機制僅限於一些已知安全的例外(操作)。

  • HttpClient不會嘗試從任何邏輯或HTTP協議錯誤(從HttpException類派生的錯誤)中恢復。
  • HttpClient會自動重試那些被認爲是冪等的方法。
  • 當HTTP請求仍然被傳送到目標服務器(即請求沒有完全傳送到服務器)時,HttpClient會自動重試那些傳送異常失敗的方法。

請求重試

爲了啓用自定義的異常恢復機制,應該提供一個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。

中止請求

在某些情況下,由於目標服務器的高負載或客戶端發出的太多併發請求,HTTP請求執行無法在預期的時間範圍內完成。在這種情況下,可能需要提前終止請求,並解除在I / O操作中阻塞的執行線程。由HttpClient執行的HTTP請求可以通過調用HttpUriRequest#abort()方法在任何執行階段中止。這個方法是線程安全的,可以從任何線程調用。當一個HTTP請求被中止時,它的執行線程 - 即使當前被阻塞在一個I / O操作中 - 通過拋出一個InterruptedIOException來保證解除阻塞。

處理重定向

HttpClient自動處理所有類型的重定向,除了HTTP規範明確禁止的重定向,需要用戶干預。請參閱其他(狀態碼303)重定向POST和PUT請求轉換爲HTTP請求所需的GET請求。可以使用自定義重定向策略來放寬對由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();
}

連接管理

連接持久性

建立從一個主機到另一個主機的連接的過程相當複雜,並且涉及兩個端點之間的多個分組交換,這可能相當耗時。連接握手的開銷可能很大,特別是對於小型的HTTP消息。如果可以重新使用開放連接來執行多個請求,則可以實現更高的數據吞吐量。

HTTP / 1.1規定HTTP連接可以重複用於多個請求。符合HTTP / 1.0的端點還可以使用一種機制來顯式傳達它們的首選項,以保持連接的活動狀態並將其用於多個請求。HTTP代理還可以保持空閒連接在一段時間內保持活動狀態,以便後續請求連接到同一個目標主機。保持連接的能力通常被稱爲連接持久性。HttpClient完全支持連接持久性。

HTTP連接路由

HttpClient能夠直接或通過可能涉及多箇中間連接(也稱爲中繼)的路由建立到目標主機的連接。

HttpClient將路由的連接區分爲普通,隧道和分層。使用多箇中間代理來隧道連接到目標主機被稱爲代理鏈接。

plain路由是直接連接到目標或只經過一個代理連接到目標服務器建立的。隧道路由(Tunnelled routes)是通過連接到第一個隧道,並通過代理鏈向目標隧道建立的。沒有代理的路由不能被隧道化。分層路由(Layered routes)通過在現有連接上分層協議來建立。協議只能在通往目標的隧道上進行分層,或者通過無代理的直接連接進行分層。

路由計算

RouteInfo接口包含到達目標主機的一個路由的信息,該路由涉及一個或多箇中間步驟或跳躍。HttpRoute是RouteInfo的具體實現,它不能被改變(是不可變的)。HttpTracker是HttpClient內部使用的一個可變的RouteInfo實現,用於跟蹤剩餘的跳轉到最終的路由目標。HttpTracker可以在向路由目標成功執行下一跳之後更新。HttpRouteDirector是一個輔助類,可以用來計算路由中的下一步。這個類由HttpClient在內部使用。

HttpRoutePlanner是一個接口,它代表一個基於執行上下文來計算給定目標的完整路由的策略。HttpClient附帶兩個默認的HttpRoutePlanner實現。SystemDefaultRoutePlanner基於java.net.ProxySelector。默認情況下,它將從系統屬性或運行應用程序的瀏覽器中獲取JVM的代理設置。DefaultProxyRoutePlanner實現不使用任何Java系統屬性,也不使用任何系統或瀏覽器代理設置。它總是通過相同的默認代理來計算路由。

安全的HTTP連接

如果在兩個連接端點之間傳輸的信息不能被未經授權的第三方讀取或篡改,那麼HTTP連接可以被認爲是安全的。SSL / TLS協議是確保HTTP傳輸安全性的最廣泛使用的技術。但是,也可以使用其他加密技術。通常,HTTP傳輸被SSL / TLS加密連接分層。

HTTP連接管理器

連接的管理和連接管理器

HTTP連接是複雜的,有狀態的,線程不安全的對象,需要妥善管理才能正常工作。HTTP連接一次只能由一個執行線程使用。HttpClient使用一個特殊的實體來管理HTTP連接的訪問,稱爲HTTP連接管理器,並由HttpClientConnectionManager接口表示。HTTP連接管理器的目的是作爲新的HTTP連接的工廠,管理持久連接的生命週期,並同步對持久連接的訪問,以確保一次只有一個線程可以訪問連接。內部HTTP連接管理器與ManagedHttpClientConnection實例一起工作,作爲管理連接狀態和控制I / O操作執行的真實連接的代理。如果託管連接被釋放或被其消費者明確關閉,則底層連接從其代理分離,並返回給管理器。即使服務消費者仍然持有對代理實例的引用,它不再有意或無意地執行任何I / O操作或改變真實連接的狀態。

這是從連接管理器獲取連接的示例:

HttpClientContext context = HttpClientContext.create();
HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
// Request new connection. This can be a long process
ConnectionRequest connRequest = connMrg.requestConnection(route, null);
// Wait for connection up to 10 sec
HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
try {
    // If not open
    if (!conn.isOpen()) {
        // establish connection based on its route info
        connMrg.connect(conn, route, 1000, context);
        // and mark it as route complete
        connMrg.routeComplete(conn, route, context);
    }
    // Do useful things with the connection.
} finally {
    connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
}

如有必要,可以通過調用ConnectionRequest#cancel()來過早終止連接請求。這將解除在ConnectionRequest#get()方法中阻塞的線程。

簡單連接管理

BasicHttpClientConnectionManager是一個簡單的連接管理器,一次只維護一個連接。即使這個類是線程安全的,它也只能被一個執行線程使用。BasicHttpClientConnectionManager將努力重複使用相同路由的後續請求的連接。但是,如果持續連接的路由與連接請求的路由不匹配,它將關閉現有連接並重新打開給定路由。如果連接已被分配,則引發java.lang.IllegalStateException。

這個連接管理器的實現應該在EJB容器中使用。

連接池連接管理

PoolingHttpClientConnectionManager是一個更復雜的實現,它管理一個客戶端連接池,並能夠處理來自多個執行線程的連接請求。連接按照其路由進行彙集。對於管理器已經在池中具有持續連接的路由的請求將通過從池租用連接而不是創建全新的連接來進行服務。

PoolingHttpClientConnectionManager保持針對一個路由的連接和所有連接的最大限制。默認情況下,每個給定的路由創建不超過2個併發連接,總共不超過20個連接。對於許多真實世界的應用程序來說,這些限制可能被證明過於嚴格,特別是如果他們使用HTTP作爲其服務的傳輸協議。

此示例顯示連接池參數如何調整:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);

CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

關閉連接管理器

當一個HttpClient實例不再需要並且即將離開作用域時,關閉它的連接管理器以確保管理器內活着的所有連接關閉並釋放由這些連接分配的系統資源是非常重要的。

CloseableHttpClient httpClient = <...>
httpClient.close();

多線程執行請求

當配備PoolingClientConnectionManager等連接池管理器時,可以使用HttpClient同時使用多個執行線程執行多個請求。

PoolingClientConnectionManager將根據其配置分配連接。如果給定路由的所有連接已經租用,連接請求將被阻塞,直到連接釋放回池。可以通過將“http.conn-manager.timeout”設置爲正值來確保連接管理器不會在連接請求操作中無限期地阻塞。如果連接請求在給定的時間內無法被服務,則拋出ConnectionPoolTimeoutException。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

// URIs to perform GETs on
String[] urisToGet = {
    "http://www.domain1.com/",
    "http://www.domain2.com/",
    "http://www.domain3.com/",
    "http://www.domain4.com/"
};

// create a thread for each URI
GetThread[] threads = new GetThread[urisToGet.length];
for (int i = 0; i < threads.length; i++) {
    HttpGet httpget = new HttpGet(urisToGet[i]);
    threads[i] = new GetThread(httpClient, httpget);
}

// start the threads
for (int j = 0; j < threads.length; j++) {
    threads[j].start();
}

// join the threads
for (int j = 0; j < threads.length; j++) {
    threads[j].join();
}

雖然HttpClient實例是線程安全的並且可以在多個執行線程之間共享,但強烈建議每個線程維護自己的HttpContext專用實例。

static class GetThread extends Thread {

    private final CloseableHttpClient httpClient;
    private final HttpContext context;
    private final HttpGet httpget;

    public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
        this.httpClient = httpClient;
        this.context = HttpClientContext.create();
        this.httpget = httpget;
    }

    @Override
    public void run() {
        try {
            CloseableHttpResponse response = httpClient.execute(
                    httpget, context);
            try {
                HttpEntity entity = response.getEntity();
            } finally {
                response.close();
            }
        } catch (ClientProtocolException ex) {
            // Handle protocol errors
        } catch (IOException ex) {
            // Handle I/O errors
        }
    }

}

連接驅逐策略

經典阻塞I / O模型的主要缺點之一是網絡套接字只有在I / O操作中被阻塞時才能對I / O事件做出反應。當一個連接釋放回管理器時,它可以保持活動狀態,但是它無法監視套接字的狀態並對任何I / O事件做出反應。如果連接在服務器端被關閉,客戶端連接將無法檢測到連接狀態的變化(並通過關閉套接字來適當地作出反應)。

HttpClient試圖在使用連接執行HTTP請求之前測試連接是否是“陳舊的”,來緩解這個問題,陳舊的連接不再有效,因爲它是在服務器端關閉。陳舊的連接檢查不是100%可靠的。唯一可行解決方案是,對於空閒連接,使用一個專用的監視器線程,該線程是不涉及每個套接字模型的一個線程,用於驅除由於長時間不活動而被認爲已過期的連接。監視線程可以定期調用ClientConnectionManager#closeExpiredConnections()方法關閉所有過期的連接,並從池中驅逐關閉的連接。它還可以選擇調用ClientConnectionManager#closeIdleConnections()方法來關閉在給定時間段內閒置的所有連接。

public static class IdleConnectionMonitorThread extends Thread {

    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;

    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }

    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }

}

連接保活策略

HTTP規範沒有指定持續連接可能會保持多久,應該保持活動狀態。一些HTTP服務器使用一個非標準的Keep-Alive標頭來向客戶端傳達他們希望在服務器端保持連接的時間段(以秒爲單位)。如果可用的話,HttpClient使用這個信息。如果響應中不存在Keep-Alive頭,則HttpClient假定連接可以無限期地保持活動狀態。但是,通常使用的許多HTTP服務器被配置爲在一段時間不活動之後丟棄持久連接,以節省系統資源,而通常不通知客戶端。如果默認策略過於樂觀,則可能需要提供自定義保活策略。

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {

    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        // Honor 'keep-alive' header
        HeaderElementIterator it = new BasicHeaderElementIterator(
                response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            HeaderElement he = it.nextElement();
            String param = he.getName();
            String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                try {
                    return Long.parseLong(value) * 1000;
                } catch(NumberFormatException ignore) {
                }
            }
        }
        HttpHost target = (HttpHost) context.getAttribute(
                HttpClientContext.HTTP_TARGET_HOST);
        if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
            // Keep alive for 5 seconds only
            return 5 * 1000;
        } else {
            // otherwise keep alive for 30 seconds
            return 30 * 1000;
        }
    }

};
CloseableHttpClient client = HttpClients.custom()
        .setKeepAliveStrategy(myStrategy)
        .build();

連接套接字工廠

HTTP連接在內部使用java.net.Socket對象來處理通過線路傳輸的數據。但是他們依靠ConnectionSocketFactory接口來創建,初始化和連接套接字。這使得HttpClient的用戶可以在運行時提供特定於應用程序的套接字初始化代碼。PlainConnectionSocketFactory是創建和初始化普通(未加密)套接字的默認工廠。創建套接字並將其連接到主機的過程是分離的,以便在連接操作中阻塞套接字。

HttpClientContext clientContext = HttpClientContext.create();
PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
Socket socket = sf.createSocket(clientContext);
int timeout = 1000; //ms
HttpHost target = new HttpHost("localhost");
InetSocketAddress remoteAddress = new InetSocketAddress(
        InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);
sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);

安全的套接字分層

LayeredConnectionSocketFactory是ConnectionSocketFactory接口的擴展。分層的套接字工廠能夠在現有的普通套接字上創建套接字。套接字分層主要用於通過代理創建安全套接字。HttpClient附帶實現SSL / TLS分層的SSLSocketFactory。請注意HttpClient不使用任何自定義加密功能。它完全依賴於標準的Java加密(JCE)和安全套接字(JSEE)擴展。

與連接管理器集成

自定義連接套接字工廠可以與特定協議方案(如HTTP或HTTPS)相關聯,然後用於創建自定義連接管理器。

ConnectionSocketFactory plainsf = <...>
LayeredConnectionSocketFactory sslsf = <...>
Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
        .register("http", plainsf)
        .register("https", sslsf)
        .build();

HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
HttpClients.custom()
        .setConnectionManager(cm)
        .build();

SSL/TLS定製

HttpClient使用SSLConnectionSocketFactory創建SSL連接。 SSLConnectionSocketFactory允許高度的自定義。它可以將javax.net.ssl.SSLContext的實例作爲參數,並使用它創建自定義配置的SSL連接。

KeyStore myTrustStore = <...>
SSLContext sslContext = SSLContexts.custom()
        .loadTrustMaterial(myTrustStore)
        .build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);

SSLConnectionSocketFactory的定製意味着對SSL / TLS協議的概念有一定程度的熟悉,其詳細解釋超出了本文檔的範圍。有關javax.net.ssl.SSLContext和相關工具的詳細說明,請參閱Java™安全套接字擴展(JSSE)參考指南。

主機名驗證

除了在SSL / TLS協議級別上執行信任驗證和客戶端身份驗證之外,HttpClient還可以選擇驗證目標主機名是否與存儲在服務器X.509證書內的名稱匹配。該驗證可以提供對服務器信任材料的真實性的附加保證。javax.net.ssl.HostnameVerifier接口表示主機名驗證策略。HttpClient提供了兩個javax.net.ssl.HostnameVerifier實現。重要提示:主機名驗證不應與SSL信任驗證混淆。

  • DefaultHostnameVerifier: HttpClient使用的默認實現符合RFC 2818。主機名必須與證書指定的備選名稱相匹配,或者在沒有給出備選名稱的情況下,證書主體的最具體的CN。通配符可以出現在CN和任何主體中。
  • NoopHostnameVerifier: 這個主機名驗證者本質上關閉主機名驗證。它接受任何有效的SSL會話並匹配目標主機。

默認情況下,HttpClient使用DefaultHostnameVerifier實現。如果需要,可以指定一個不同的主機名驗證器實現:

SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslContext,
        NoopHostnameVerifier.INSTANCE);
  • 從版本4.4開始,HttpClient使用由Mozilla基金會友好維護的公共後綴列表,以確保SSL證書中的通配符不會被濫用以應用於具有公共頂級域的多個域。HttpClient附帶在發佈時檢索的列表的副本。列表的最新版本可以在https://publicsuffix.org/list/找到。建議清單的本地副本,並從其原始位置每天下載一次,這是非常值得建議的。
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(
    PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat"));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);

通過使用空匹配器,可以禁止對公衆足跡進行驗證。

DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(null);
  •  

HTTP代理配置

即使HttpClient知道複雜的路由方案和代理鏈,它只支持簡單的直接或一跳代理連接。

告訴HttpClient通過代理連接到目標主機的最簡單的方法是設置默認的代理參數:

HttpHost proxy = new HttpHost("someproxy", 8080);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();

也可以指示HttpClient使用標準JRE代理選擇器來獲取代理信息:

SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(
        ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();

或者,可以提供自定義的RoutePlanner實現,以完全控制HTTP路由計算過程:

HttpRoutePlanner routePlanner = new HttpRoutePlanner() {

    public HttpRoute determineRoute(
            HttpHost target,
            HttpRequest request,
            HttpContext context) throws HttpException {
        return new HttpRoute(target, null,  new HttpHost("someproxy", 8080),
                "https".equalsIgnoreCase(target.getSchemeName()));
    }

};
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();
    }
}

HTTP狀態管理

最初,HTTP被設計爲一種無狀態的,面向請求/響應的協議,它並沒有爲跨越多個邏輯相關的請求/響應交換的有狀態會話做出特殊的規定。隨着HTTP協議越來越流行,越來越多的系統開始將其用於應用程序,例如用於電子商務應用程序的傳輸。因此,對狀態管理的支持變成一種必然。

當時,網景通信是一家領先的網絡客戶端和服務器軟件開發商,它們在其產品基於專有規範的基礎上實現了對HTTP狀態管理的支持。後來,Netscape試圖通過發佈規範草案來規範這個機制。這些努力有助於通過RFC標準軌道定義的正式規範。但是,大量應用程序中的狀態管理仍然主要基於Netscape草案,並且與官方規範不兼容。網絡瀏覽器的所有主要開發者都不得不保持與這些應用程序的兼容性,這些應用程序極大地促成了標準遵從性的分裂。

HTTP cookie是HTTP代理和目標服務器可以交換來維護會話的令牌或短包狀態信息。網景工程師曾經把它稱爲“魔術餅乾”。 HttpClient使用Cookie接口來表示一個抽象的cookie標記。HTTP Cookie的最簡單的形式就是一個鍵/值對。通常,HTTP cookie還包含許多屬性,例如有效的域,指定應用此cookie的原始服務器上URL的子集的路徑以及cookie有效的最長時間。

SetCookie接口表示由原始服務器發送給HTTP代理的一個Set-Cookie響應頭,以保持對話狀態。

ClientCookie接口擴展了Cookie接口和額外的客戶端特定功能,比如能夠完全按照原始服務器指定的方式檢索原始Cookie屬性。這對於生成Cookie標題非常重要,因爲某些Cookie規範要求只有在Set-Cookie標頭中指定Cookie標頭時,Cookie標頭才應包含某些屬性。

這裏是創建一個客戶端cookie對象的例子:

BasicClientCookie cookie = new BasicClientCookie("name", "value");
// Set effective domain and path attributes
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
// Set attributes exactly as sent by the server
cookie.setAttribute(ClientCookie.PATH_ATTR, "/");
cookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");

Cookie規範

CookieSpec接口代表一個cookie管理規範。 cookie管理規範預計將執行:

  • 解析Set-Cookie標題的規則
  • 解析Cookie的驗證規則
  • 給定的主機,端口和原始路徑的Cookie頭的格式。

HttpClient附帶幾個CookieSpec實現:

  • Standard strict: 狀態管理策略符合RFC 6265第4節定義的良好行爲配置文件的語法和語義。
  • Standard: 狀態管理策略符合RFC 6265第4節定義的更爲寬鬆的配置文件,旨在與不符合良好行爲的配置文件的現有服務器進行互操作。
  • Netscape draft (obsolete): 此策咯符合Netscape Communications公佈的原始草案規範。除非絕對有必要與遺留代碼兼容,否則應該避免。
  • RFC 2965 (obsolete): 狀態管理策略符合RFC 2965定義的過時狀態管理規範。請不要在新的應用程序中使用。
  • RFC 2109 (obsolete): 狀態管理策略符合RFC 2109定義的過時狀態管理規範。請不要在新的應用程序中使用。
  • Browser compatibility (obsolete): 該策略致力於模仿老版本瀏覽器應用程序(如Microsoft Internet Explorer和Mozilla FireFox)的(錯誤)行爲。請不要在新的應用程序中使用。
  • Default: 默認的cookie策略是一種綜合的策略,根據HTTP響應發送的cookie的屬性(如版本屬性,現在已經過時),選擇符合RFC 2965,RFC 2109或Netscape草案的兼容實現。在下一個次版本的HttpClient中,這個策略將被棄用,以支持標準(符合RFC 6265)實現。
  • Ignore cookies: 所有的cookies都被忽略。

強烈建議在新應用程序中使用標準或嚴格的嚴格策略。過時的規格只能用於與舊系統的兼容性。對於過時的規範的支持將在下一個主要版本的HttpClient中被刪除。

選擇Cookie策略

可以在HTTP客戶端上設置Cookie策略,並根據需要在HTTP請求級別上重寫Cookie策略。

RequestConfig globalConfig = RequestConfig.custom()
        .setCookieSpec(CookieSpecs.DEFAULT)
        .build();
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultRequestConfig(globalConfig)
        .build();
RequestConfig localConfig = RequestConfig.copy(globalConfig)
        .setCookieSpec(CookieSpecs.STANDARD_STRICT)
        .build();
HttpGet httpGet = new HttpGet("/");
httpGet.setConfig(localConfig);

自定義Cookie策略

爲了實現自定義Cookie策略,應該創建一個CookieSpec接口的自定義實現,創建一個CookieSpecProvider實現來創建和初始化自定義規範的實例,並用HttpClient註冊工廠。一旦自定義規範已經註冊,就可以像標準cookie規範一樣激活它。

PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();

Registry<CookieSpecProvider> r = RegistryBuilder.<CookieSpecProvider>create()
        .register(CookieSpecs.DEFAULT,
                new DefaultCookieSpecProvider(publicSuffixMatcher))
        .register(CookieSpecs.STANDARD,
                new RFC6265CookieSpecProvider(publicSuffixMatcher))
        .register("easy", new EasySpecProvider())
        .build();

RequestConfig requestConfig = RequestConfig.custom()
        .setCookieSpec("easy")
        .build();

CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultCookieSpecRegistry(r)
        .setDefaultRequestConfig(requestConfig)
        .build();
  • Cookie持久性

HttpClient可以使用實現CookieStore接口的持久性cookie存儲的任何物理表示。名爲BasicCookieStore的默認CookieStore實現是一個由java.util.ArrayList支持的簡單實現。存儲在BasicClientCookie對象中的cookie在容器對象被垃圾收集時會丟失。用戶可以根據需要提供更復雜的實現。

// Create a local instance of cookie store
CookieStore cookieStore = new BasicCookieStore();
// Populate cookies if needed
BasicClientCookie cookie = new BasicClientCookie("name", "value");
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
cookieStore.addCookie(cookie);
// Set the store
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultCookieStore(cookieStore)
        .build();

HTTP狀態管理和執行上下文

在HTTP請求執行過程中,HttpClient將以下與狀態管理相關的對象添加到執行上下文中:

  • Lookup 代表實際的cookie規範註冊表的實例。在本地上下文中設置的這個屬性的值優先於默認值。
  • CookieSpec 代表實際cookie規範的實例。
  • CookieOrigin 實例代表原始服務器的實際細節。
  • CookieStore 代表實際cookie存儲的實例。在本地上下文中設置的這個屬性的值優先於默認值。

本地HttpContext對象可用於在請求執行之前自定義HTTP狀態管理上下文,或在請求執行後檢查其狀態。也可以使用單獨的執行上下文來實現每個用戶(或每個線程)的狀態管理。在本地上下文中定義的cookie規範註冊表和cookie存儲優先於在HTTP客戶端級別設置的默認規則。

CloseableHttpClient httpclient = <...>

Lookup<CookieSpecProvider> cookieSpecReg = <...>
CookieStore cookieStore = <...>

HttpClientContext context = HttpClientContext.create();
context.setCookieSpecRegistry(cookieSpecReg);
context.setCookieStore(cookieStore);
HttpGet httpget = new HttpGet("http://somehost/");
CloseableHttpResponse response1 = httpclient.execute(httpget, context);
<...>
// Cookie origin details
CookieOrigin cookieOrigin = context.getCookieOrigin();
// Cookie spec used
CookieSpec cookieSpec = context.getCookieSpec();

HTTP身份驗證

HttpClient完全支持由HTTP標準規範定義的認證方案以及許多廣泛使用的非標準認證方案,如NTLM和SPNEGO。

用戶憑證

任何用戶身份驗證過程都需要一組可用於建立用戶身份的憑證。最簡單的形式是用戶憑證可以只是一個用戶名/密碼對。

UsernamePasswordCredentials表示由明文形式的安全主體和密碼組成的一組憑證。這個實現對於HTTP標準規範定義的標準認證方案是足夠的。

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("user", "pwd");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

// 輸出
user
pwd

NTCredentials是一個特定於Microsoft Windows的實現,除了用戶名/密碼對之外,還包括一組額外的Windows特定屬性,例如用戶域的名稱。在Microsoft Windows網絡中,同一用戶可以屬於多個域,每個域都有一組不同的授權。

NTCredentials creds = new NTCredentials("user", "pwd", "workstation", "domain");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

// 輸出
DOMAIN/user
pwd
  • 認證方案

AuthScheme接口表示抽象的面向質詢 - 響應的認證方案。認證方案將支持以下功能:

  • 解析和處理目標服務器發送的質詢,以響應受保護資源的請求。
  • 提供處理後的挑戰的屬性:認證方案類型及其參數,如認證方案適用的領域(如果可用)
  • 爲給定的一組憑證和HTTP請求生成授權字符串以響應實際的授權質詢。

請注意,身份驗證方案可能是有狀態的,質詢一系列挑戰 - 響應交換。

HttpClient附帶有幾個AuthScheme實現:

  • Basic: RFC 2617中定義的基本認證方案。這種認證方案是不安全的,因爲憑證是以明文形式傳輸的。儘管不安全基本身份驗證方案如果與TLS / SSL加密結合使用,則完全可以滿足要求。
  • Digest: 摘要認證方案在RFC 2617中定義。摘要式身份驗證方案比Basic更安全,對於那些不希望通過TLS / SSL加密實現完全傳輸安全性開銷的應用程序來說,它可能是一個不錯的選擇。
  • NTLM: NTLM是Microsoft開發的專用身份驗證方案,針對Windows平臺進行了優化。 NTLM被認爲比Digest更安全。
  • SPNEGO: SPNEGO(簡單和受保護的GSSAPI協商機制)是一種GSSAPI“僞機制”,用於協商許多可能的實際機制之一。SPNEGO最明顯的用途是在Microsoft的HTTP協商認證擴展中。可協商的子機制包括由Active Directory支持的NTLM和Kerberos。目前HttpClient只支持Kerberos子機制。
  • Kerberos: Kerberos身份驗證實現。

憑證供應商

憑證提供程序旨在維護一組用戶憑證,並能夠爲特定的認證範圍生成用戶憑證。身份驗證範圍由主機名,端口號,領域名稱和身份驗證方案名稱組成。當向憑證提供者註冊憑證時,可以提供通配符(任何主機,任何端口,任何領域,任何方案)而不是具體的屬性值。如果無法找到直接匹配,憑證提供程序將希望能夠找到特定範圍的最接近的匹配項。
HttpClient可以處理實現CredentialsProvider接口的憑證提供者的任何物理表示。名爲BasicCredentialsProvider的默認CredentialsProvider實現是一個由java.util.HashMap支持的簡單實現。

CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
    new AuthScope("somehost", AuthScope.ANY_PORT), 
    new UsernamePasswordCredentials("u1", "p1"));
credsProvider.setCredentials(
    new AuthScope("somehost", 8080), 
    new UsernamePasswordCredentials("u2", "p2"));
credsProvider.setCredentials(
    new AuthScope("otherhost", 8080, AuthScope.ANY_REALM, "ntlm"), 
    new UsernamePasswordCredentials("u3", "p3"));

System.out.println(credsProvider.getCredentials(
    new AuthScope("somehost", 80, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("somehost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("otherhost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("otherhost", 8080, null, "ntlm")));

// 輸出
[principal: u1]
[principal: u2]
null
[principal: u3]

HTTP認證和執行上下文

HttpClient依靠AuthState類來跟蹤有關身份驗證過程的詳細信息。HttpClient在HTTP請求執行過程中創建兩個AuthState實例:一個用於目標主機身份驗證,另一個用於代理身份驗證。如果目標服務器或代理需要用戶身份驗證,則相應的AuthScope實例將使用在身份驗證過程中使用的AuthScope,AuthScheme和Crednetials來填充。可以檢查AuthState以查明請求的身份驗證類型,是否找到匹配的AuthScheme實現以及憑證提供程序是否設法找到給定身份驗證範圍的用戶憑證。

在HTTP請求執行過程中,HttpClient將以下與認證相關的對象添加到執行上下文中:

  • Lookup 表示實際認證方案註冊表的實例。在本地上下文中設置的這個屬性的值優先於默認值。
  • CredentialsProvider 表示實際憑據提供者的實例。在本地上下文中設置的這個屬性的值優先於默認值。
  • AuthState 表示實際目標驗證狀態的實例。在本地上下文中設置的這個屬性的值優先於默認值。
  • AuthState 表示實際代理身份驗證狀態的實例。在本地上下文中設置的這個屬性的值優先於默認值。
  • AuthCache 表示實際認證數據緩存的實例。在本地上下文中設置的這個屬性的值優先於默認值。

本地HttpContext對象可用於在請求執行之前自定義HTTP認證上下文,或者在請求執行後檢查其狀態:

CloseableHttpClient httpclient = <...>

CredentialsProvider credsProvider = <...>
Lookup<AuthSchemeProvider> authRegistry = <...>
AuthCache authCache = <...>

HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthSchemeRegistry(authRegistry);
context.setAuthCache(authCache);
HttpGet httpget = new HttpGet("http://somehost/");
CloseableHttpResponse response1 = httpclient.execute(httpget, context);
<...>

AuthState proxyAuthState = context.getProxyAuthState();
System.out.println("Proxy auth state: " + proxyAuthState.getState());
System.out.println("Proxy auth scheme: " + proxyAuthState.getAuthScheme());
System.out.println("Proxy auth credentials: " + proxyAuthState.getCredentials());
AuthState targetAuthState = context.getTargetAuthState();
System.out.println("Target auth state: " + targetAuthState.getState());
System.out.println("Target auth scheme: " + targetAuthState.getAuthScheme());
System.out.println("Target auth credentials: " + targetAuthState.getCredentials());

認證數據的緩存

從版本4.1開始,HttpClient會自動緩存已成功驗證的主機信息。請注意,必須使用相同的執行上下文來執行邏輯相關的請求,以便將緩存的認證數據從一個請求傳播到另一個請求。執行上下文超出範圍後,身份驗證數據將立即丟失。

搶先認證(Preemptive authentication)

HttpClient不支持開箱即用的認證,因爲如果誤用或使用不當,搶先認證可能導致重大的安全問題,例如以明文形式將用戶憑據發送給未經授權的第三方。因此,希望用戶在特定的應用環境中評估搶先認證與安全風險的潛在益處。

但是可以通過預先填充認證數據緩存來配置HttpClient進行搶先認證。

CloseableHttpClient httpclient = <...>

HttpHost targetHost = new HttpHost("localhost", 80, "http");
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
        new AuthScope(targetHost.getHostName(), targetHost.getPort()),
        new UsernamePasswordCredentials("username", "password"));

// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);

// Add AuthCache to the execution context
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthCache(authCache);

HttpGet httpget = new HttpGet("/");
for (int i = 0; i < 3; i++) {
    CloseableHttpResponse response = httpclient.execute(
            targetHost, httpget, context);
    try {
        HttpEntity entity = response.getEntity();

    } finally {
        response.close();
    }
}
  • NTLM身份驗證

從版本4.1開始,HttpClient提供對NTLMv1,NTLMv2和NTLM2會話認證的全面支持。人們仍然可以繼續使用由Samba項目開發的外部NTLM引擎(如JCIFS庫)作爲其Windows互操作性套件程序的一部分。

NTLM連接持久性

與標準的基本和摘要方案相比,NTLM認證方案在計算開銷和性能影響方面明顯更昂貴。這可能是微軟選擇使NTLM身份驗證方案處於有狀態的主要原因之一。也就是說,一旦通過身份驗證,用戶身份就與該連接的整個使用期限相關聯。NTLM連接的有狀態性使得連接持久性更加複雜,因爲持久性NTLM連接的明顯原因可能不會被具有不同用戶身份的用戶重新使用。HttpClient附帶的標準連接管理器完全能夠管理有狀態的連接。然而,在同一個會話中,邏輯上相關的請求使用相同的執行上下文以使他們知道當前的用戶身份是非常重要的。否則,HttpClient將最終爲每個針對NTLM保護資源的HTTP請求創建一個新的HTTP連接。有關有狀態HTTP連接的詳細討論,請參閱本節。

由於NTLM連接是有狀態的,因此通常建議使用相對便宜的方法來觸發NTLM身份驗證,如GET或HEAD,並重新使用相同的連接執行更昂貴的方法,特別是那些包含請求實體(如POST或PUT)的方法。

CloseableHttpClient httpclient = <...>

CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY,
        new NTCredentials("user", "pwd", "myworkstation", "microsoft.com"));

HttpHost target = new HttpHost("www.microsoft.com", 80, "http");

// Make sure the same context is used to execute logically related requests
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);

// Execute a cheap method first. This will trigger NTLM authentication
HttpGet httpget = new HttpGet("/ntlm-protected/info");
CloseableHttpResponse response1 = httpclient.execute(target, httpget, context);
try {
    HttpEntity entity1 = response1.getEntity();
} finally {
    response1.close();
}

// Execute an expensive method next reusing the same context (and connection)
HttpPost httppost = new HttpPost("/ntlm-protected/form");
httppost.setEntity(new StringEntity("lots and lots of data"));
CloseableHttpResponse response2 = httpclient.execute(target, httppost, context);
try {
    HttpEntity entity2 = response2.getEntity();
} finally {
    response2.close();
}
  •  

SPNEGO / Kerberos身份驗證

SPNEGO(簡單和受保護的GSSAPI協商機制)被設計爲當兩端都不知道對方能夠使用/提供什麼時,允許對服務進行認證。這是最常用的Kerberos身份驗證。它可以包裝其他機制,但是HttpClient中的當前版本僅僅考慮了Kerberos。

HttpClient對SPNEGO的支持

SPNEGO身份驗證方案與Sun Java 1.5及更高版本兼容。但強烈建議使用Java> = 1.6,因爲它更完整地支持SPNEGO身份驗證。Sun JRE提供了幾乎所有的Kerberos和SPNEGO令牌處理的支持類。這意味着很多設置是針對GSS類的。 SPNegoScheme是一個簡單的類來處理令牌的編組和讀寫正確的頭文件。

最好的方法是在示例中獲取KerberosHttpClient.java文件,並嘗試使其運行。有很多問題可以發生,但如果幸運的話,它會工作,沒有太多的問題。它也應該提供一些輸出來調試。

在Windows中,它應該默認使用登錄的憑據;這可以通過使用例如’kinit’來覆蓋。 $ JAVA_HOME \ bin \ kinit [email protected],這對測試和調試問題非常有幫助。刪除由kinit創建的緩存文件,以恢復到Windows Kerberos緩存。

確保在krb5.conf文件中列出domain_realms。這是問題的主要來源。

GSS / Java Kerberos安裝程序

本文檔假設您使用的是Windows,但大部分信息也適用於Unix。org.ietf.jgss類有很多可能的配置參數,主要是在krb5.conf / krb5.ini文件中。有關http://web.mit.edu/kerberos/krb5-1.4/krb5-1.4.1/doc/krb5-admin/krb5.conf.html格式的更多信息。

  • Client Web Browser does HTTP GET for resource.
  • Web server returns HTTP 401 status and a header: WWW-Authenticate: Negotiate
  • Client generates a NegTokenInit, base64 encodes it, and resubmits the GET with an Authorization header: Authorization: Negotiate .
  • Server decodes the NegTokenInit, extracts the supported MechTypes (only Kerberos V5 in our case), ensures it is one of the expected ones, and then extracts the MechToken (Kerberos Token) and authenticates it.
    If more processing is required another HTTP 401 is returned to the client with more data in the the WWW-Authenticate header. Client takes the info and generates another token passing this back in the Authorization header until complete.
  • When the client has been authenticated the Web server should return the HTTP 200 status, a final WWW-Authenticate header and the page content.

login.conf 文件

以下配置是在Windows XP中針對IIS和JBoss協商模塊的基本設置。
系統屬性java.security.auth.login.config可以用來指向login.conf文件。

login.conf內容可能如下所示:

com.sun.security.jgss.login {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

com.sun.security.jgss.initiate {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

com.sun.security.jgss.accept {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

krb5.conf / krb5.ini文件

如果未指定,將使用系統默認值。通過設置系統屬性java.security.krb5.conf指向一個自定義的krb5.conf文件來覆蓋(如果需要的話)。 krb5.conf的內容可能如下所示:

[libdefaults]
    default_realm = AD.EXAMPLE.NET
    udp_preference_limit = 1
[realms]
    AD.EXAMPLE.NET = {
        kdc = KDC.AD.EXAMPLE.NET
    }
[domain_realms]
.ad.example.net=AD.EXAMPLE.NET
ad.example.net=AD.EXAMPLE.NET

Windows特定配置

要允許Windows使用當前用戶的票據,必須將系統屬性javax.security.auth.useSubjectCredsOnly設置爲false,並且應該添加並正確設置Windows註冊表項allowtgtsessionkey,以允許在Kerberos票證授予票證中發送會話密鑰。

在Windows Server 2003和Windows 2000 SP4上,這裏是所需的註冊表設置:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters
Value Name: allowtgtsessionkey
Value Type: REG_DWORD
Value: 0x01
  •  

以下是Windows XP SP2中註冊表設置的位置:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\
Value Name: allowtgtsessionkey
Value Type: REG_DWORD
Value: 0x01
  •  

流API(fluent api)

易於使用的API

從4.2版開始,HttpClient基於流暢的界面概念提供了一個易於使用的Facade API。Fluent Facade API只公開了HttpClient的最基本的功能,並且適用於不需要HttpClient的全部靈活性的簡單用例。例如,流暢的Facade API可以讓用戶不必處理連接管理和資源釋放。

以下是通過HC fluent API執行的HTTP請求的幾個示例:

// Execute a GET with timeout settings and return response content as String.
Request.Get("http://somehost/")
        .connectTimeout(1000)
        .socketTimeout(1000)
        .execute().returnContent().asString();
  •  
// Execute a POST with the 'expect-continue' handshake, using HTTP/1.1,
// containing a request body as String and return response content as byte array.
Request.Post("http://somehost/do-stuff")
        .useExpectContinue()
        .version(HttpVersion.HTTP_1_1)
        .bodyString("Important stuff", ContentType.DEFAULT_TEXT)
        .execute().returnContent().asBytes();
  •  
// Execute a POST with a custom header through the proxy containing a request body
// as an HTML form and save the result to the file
Request.Post("http://somehost/some-form")
        .addHeader("X-Custom-header", "stuff")
        .viaProxy(new HttpHost("myproxy", 8080))
        .bodyForm(Form.form().add("username", "vip").add("password", "secret").build())
        .execute().saveContent(new File("result.dump"));

還可以直接使用Executor來執行特定安全上下文中的請求,從而將身份驗證信息緩存起來並重新用於後續請求。

Executor executor = Executor.newInstance()
        .auth(new HttpHost("somehost"), "username", "password")
        .auth(new HttpHost("myproxy", 8080), "username", "password")
        .authPreemptive(new HttpHost("myproxy", 8080));

executor.execute(Request.Get("http://somehost/"))
        .returnContent().asString();

executor.execute(Request.Post("http://somehost/do-stuff")
        .useExpectContinue()
        .bodyString("Important stuff", ContentType.DEFAULT_TEXT))
        .returnContent().asString();

響應處理

流暢的Facade API通常使用戶不必處理連接管理和資源釋放。但是在大多數情況下,這是以不得不緩衝內存中響應消息的內容爲代價的。強烈建議使用ResponseHandler進行HTTP響應處理,以避免在內存中緩衝內容。

Document result = Request.Get("http://somehost/content")
        .execute().handleResponse(new ResponseHandler<Document>() {

    public Document 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");
        }
        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
            ContentType contentType = ContentType.getOrDefault(entity);
            if (!contentType.equals(ContentType.APPLICATION_XML)) {
                throw new ClientProtocolException("Unexpected content type:" +
                    contentType);
            }
            String charset = contentType.getCharset();
            if (charset == null) {
                charset = HTTP.DEFAULT_CONTENT_CHARSET;
            }
            return docBuilder.parse(entity.getContent(), charset);
        } catch (ParserConfigurationException ex) {
            throw new IllegalStateException(ex);
        } catch (SAXException ex) {
            throw new ClientProtocolException("Malformed XML document", ex);
        }
    }

    });

HTTP緩存

一般概念

HttpClient Cache提供了一個與HTTP / 1.1兼容的緩存層,可以和HttpClient一起使用 - 這是Java瀏覽器緩存的等價物。該實現遵循責任鏈設計模式,其中緩存HttpClient實現可以爲默認的非緩存HttpClient實現提供一個嵌入式替代; 完全可以從緩存滿足的請求不會導致實際的原始請求。使用條件GET和If-Modified-Since和/或If-None-Match請求標頭,過期的緩存條目將儘可能使用原始地址進行自動驗證。

一般而言,HTTP / 1.1緩存被設計爲在語義上是透明的;也就是說,緩存不應該改變客戶端和服務器之間請求 - 響應交換的含義。因此,將緩存的HttpClient放置到現有的兼容客戶端 - 服務器關係中應該是安全的。雖然緩存模塊是從HTTP協議的角度來看是客戶端的一部分,但實現的目標是與透明緩存代理的要求兼容。

最後,緩存HttpClient包括支持由RFC 5861指定的緩存控制擴展(stale-if-error和stale-while-revalidate)。

當緩存HttpClient執行請求時,它會經歷以下流程:

  • 檢查基本符合HTTP 1.1協議的請求,並嘗試更正請求。
  • 刷新任何將被此請求廢除的緩存條目。
  • 確定當前請求是否可以從緩存中獲取。如果不是,則直接將請求傳遞給原始服務器,並在適當的情況下緩存後返回響應。
  • 如果這是一個緩存可服務請求,它將嘗試從緩存中讀取它。如果不在緩存中,則調用原始服務器並緩存響應(如果適用)。
  • 如果緩存的響應適合作爲響應,則構造包含ByteArrayEntity的BasicHttpResponse並將其返回。否則,嘗試對原始服務器重新驗證緩存項。
  • 對於無法重新驗證的緩存響應,請調用原始服務器並緩存響應(如果適用)。

當緩存HttpClient收到一個響應,它會經歷以下流程:

  • 檢查協議符合性的響應
  • 確定響應是否可緩存
  • 如果它是可緩存的,則嘗試讀取配置中允許的最大大小並將其存儲在緩存中。
  • 如果緩存的響應太大,請重新構建部分消耗的響應,並直接返回而不緩存。

請注意,緩存HttpClient本身不是HttpClient的不同實現,而是通過將自身作爲附加處理組件插入到請求執行管道來工作。

RFC-2616合規性

我們相信HttpClient緩存是無條件符合RFC-2616的。也就是說,無論規範如何指示,必須,不應該,或者不應該爲HTTP緩存,緩存層試圖以滿足這些要求的方式行事。這意味着緩存模塊在放入時不會產生不正確的行爲。

用法示例

這是如何設置一個基本的緩存HttpClient的簡單例子。按照配置,它將最多存儲1000個緩存對象,每個對象的最大主體大小爲8192字節。這裏選擇的數字僅僅是舉例而已,並不是要說明性的或者被認爲是建議。

CacheConfig cacheConfig = CacheConfig.custom()
        .setMaxCacheEntries(1000)
        .setMaxObjectSize(8192)
        .build();
RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(30000)
        .setSocketTimeout(30000)
        .build();
CloseableHttpClient cachingClient = CachingHttpClients.custom()
        .setCacheConfig(cacheConfig)
        .setDefaultRequestConfig(requestConfig)
        .build();

HttpCacheContext context = HttpCacheContext.create();
HttpGet httpget = new HttpGet("http://www.mydomain.com/content/");
CloseableHttpResponse response = cachingClient.execute(httpget, context);
try {
    CacheResponseStatus responseStatus = context.getCacheResponseStatus();
    switch (responseStatus) {
        case CACHE_HIT:
            System.out.println("A response was generated from the cache with " +
                    "no requests sent upstream");
            break;
        case CACHE_MODULE_RESPONSE:
            System.out.println("The response was generated directly by the " +
                    "caching module");
            break;
        case CACHE_MISS:
            System.out.println("The response came from an upstream server");
            break;
        case VALIDATED:
            System.out.println("The response was generated from the cache " +
                    "after validating the entry with the origin server");
            break;
    }
} finally {
    response.close();
}

配置

緩存HttpClient繼承了默認非緩存實現(包括設置選項,如超時和連接池大小)的所有配置選項和參數。對於特定於緩存的配置,可以提供CacheConfig實例來自定義以下區域的行爲:

  • 緩存大小。如果後端存儲支持這些限制,則可以指定最大緩存條目數以及最大可緩存響應主體大小。
  • 公/私人緩存。默認情況下,緩存模塊認爲自己是一個共享(公共)緩存,並且不會緩存對具有“緩存控制:私人”標記的授權頭或響應的請求的響應。但是,如果高速緩存僅由一個邏輯“用戶”(與瀏覽器高速緩存類似)使用,那麼您將需要關閉共享高速緩存設置。
  • 啓發式緩存。根據RFC2616,即使沒有明確的緩存控制頭由原始設置,緩存也可以緩存某些緩存條目。這種行爲在默認情況下是關閉的,但是如果您正在使用沒有設置正確標題但您仍然想要緩存響應的源,則可能需要將其打開。您將希望啓用啓發式緩存,然後指定自上次修改資源以來的默認新鮮度生存期和/或時間的一小部分。有關啓發式緩存的更多詳細信息,請參閱HTTP / 1.1 RFC的第13.2.2和13.2.4節。
  • 後臺驗證。緩存模塊支持RFC5861的stale-while-revalidate指令,允許在後臺發生某些緩存條目重新驗證。您可能需要調整後臺工作線程的最小和最大數量的設置,以及在回收之前它們可以空閒的最長時間。當沒有足夠的工作線程跟上需求時,您還可以控制用於重新驗證的隊列大小。

存儲後端

緩存HttpClient的默認實現將緩存條目和緩存的響應實體存儲在應用程序的JVM的內存中。雖然這提供了很高的性能,但是由於大小的限制,或者由於緩存條目是短暫的,並且在應用程序重新啓動時不能存活,所以可能不適合您的應用程序。當前版本包括支持使用EhCache和memcached實現存儲緩存條目,這允許將緩存條目溢出到磁盤或將其存儲在外部進程中。

如果這些選項都不適合您的應用程序,那麼可以通過實現HttpCacheStorage接口來提供您自己的存儲後端,然後在構建時提供它來緩存HttpClient。在這種情況下,緩存條目將使用您的方案進行存儲,但您將重用所有關於HTTP / 1.1遵從性和緩存處理的邏輯。一般來說,應該可以使用支持鍵/值存儲(類似於Java Map接口)的任何東西來創建HttpCacheStorage實現,並能夠應用原子更新。

最後,通過一些額外的努力,完全有可能建立一個多層緩存層次結構; 例如,將內存緩存HttpClient包裝在磁盤上或遠程存儲在緩存中的緩存條目中,遵循類似於虛擬內存,L1 / L2處理器緩存等的模式。

高級主題

自定義客戶端連接

在某些情況下,爲了能夠處理非標準,不符合規範的行爲,可能有必要定製HTTP消息通過線路傳輸的方式,超出了使用HTTP參數進行傳輸的方式。例如,對於Web爬蟲,可能需要強制HttpClient接受格式不正確的響應頭以挽救消息的內容。

通常,插入自定義消息解析器或自定義連接實現的過程涉及以下幾個步驟:

  • 提供一個自定義的LineParser / LineFormatter接口實現。根據需要實現消息解析/格式化邏輯。
class MyLineParser extends BasicLineParser {

    @Override
    public Header parseHeader(
            CharArrayBuffer buffer) throws ParseException {
        try {
            return super.parseHeader(buffer);
        } catch (ParseException ex) {
            // Suppress ParseException exception
            return new BasicHeader(buffer.toString(), null);
        }
    }

}
  • 提供一個自定義的HttpConnectionFactory實現。將默認的請求書寫器和/或響應解析器替換爲自定義的。
HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory =
        new ManagedHttpClientConnectionFactory(
            new DefaultHttpRequestWriterFactory(),
            new DefaultHttpResponseParserFactory(
                    new MyLineParser(), new DefaultHttpResponseFactory()));
  • 配置HttpClient以使用自定義連接工廠。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
    connFactory);
CloseableHttpClient httpclient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

有狀態的HTTP連接

雖然HTTP規範假定會話狀態信息總是以HTTP cookie的形式嵌入到HTTP消息中,因此HTTP連接總是無狀態的,但這種假設在現實生活中並不總是成立。在某些情況下,HTTP連接是使用特定的用戶身份或在特定的安全上下文中創建的,因此不能與其他用戶共享,並且只能由同一用戶重用。這種有狀態的HTTP連接的示例是NTLM身份驗證連接和帶有客戶端證書身份驗證的SSL連接。

用戶令牌處理程序

HttpClient依靠UserTokenHandler接口來確定給定的執行上下文是否是用戶特定的。如果上下文是用戶特定的,則該處理程序返回的標記對象應該唯一標識當前用戶;如果上下文不包含任何特定於當前用戶的資源或細節,則爲null。用戶令牌將用於確保用戶特定資源不會與其他用戶共享或重新使用。

UserTokenHandler接口的默認實現使用Principal類的實例來表示HTTP連接的狀態對象(如果可以從給定的執行上下文中獲取的話)。DefaultUserTokenHandler將使用基於連接的身份驗證方案的用戶主體,如NTLM或SSL會話的客戶端身份驗證打開。如果兩者都不可用,則返回空令牌。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpget = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response = httpclient.execute(httpget, context);
try {
    Principal principal = context.getUserToken(Principal.class);
    System.out.println(principal);
} finally {
    response.close();
}

如果默認的用戶不滿足他們的需求,用戶可以提供一個自定義的實現:

UserTokenHandler userTokenHandler = new UserTokenHandler() {

    public Object getUserToken(HttpContext context) {
        return context.getAttribute("my-token");
    }

};
CloseableHttpClient httpclient = HttpClients.custom()
        .setUserTokenHandler(userTokenHandler)
        .build();

持久的有狀態連接

請注意,只有在執行請求時將相同的狀態對象綁定到執行上下文,才能重新使用攜帶狀態對象的持久連接。因此,確保相同的上下文被同一用戶的後續HTTP請求的執行重用,或者在請求執行之前用戶令牌被綁定到上下文是非常重要的。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context1 = HttpClientContext.create();
HttpGet httpget1 = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response1 = httpclient.execute(httpget1, context1);
try {
    HttpEntity entity1 = response1.getEntity();
} finally {
    response1.close();
}
Principal principal = context1.getUserToken(Principal.class);

HttpClientContext context2 = HttpClientContext.create();
context2.setUserToken(principal);
HttpGet httpget2 = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response2 = httpclient.execute(httpget2, context2);
try {
    HttpEntity entity2 = response2.getEntity();
} finally {
    response2.close();
}

使用FutureRequestExecutionService

使用FutureRequestExecutionService,您可以調度http調用,並將響應視爲未來。這是有用的,例如,多次調用Web服務。使用FutureRequestExecutionService的優點是可以使用多個線程同時調度請求,設置任務超時或在不再需要響應時取消它們。

FutureRequestExecutionService使用擴展FutureTask的HttpRequestFutureTask封裝請求。這個類允許您取消任務,並跟蹤各種指標,如請求持續時間。

創建FutureRequestExecutionService

futureRequestExecutionService的構造函數接受任何現有的httpClient實例和一個ExecutorService實例。配置兩者時,重要的是將最大連接數與要使用的線程數對齊。當線程比連接多時,連接可能開始超時,因爲沒有可用的連接。當連接比線程多,futureRequestExecutionService將不會使用所有它們。

HttpClient httpClient = HttpClientBuilder.create().setMaxConnPerRoute(5).build();
ExecutorService executorService = Executors.newFixedThreadPool(5);
FutureRequestExecutionService futureRequestExecutionService =
    new FutureRequestExecutionService(httpClient, executorService);

計劃請求

要安排請求,只需提供一個HttpUriRequest,HttpContext和一個ResponseHandler。由於請求由執行程序服務處理,所以必須使用ResponseHandler。

private final class OkidokiHandler implements ResponseHandler<Boolean> {
    public Boolean handleResponse(
            final HttpResponse response) throws ClientProtocolException, IOException {
        return response.getStatusLine().getStatusCode() == 200;
    }
}

HttpRequestFutureTask<Boolean> task = futureRequestExecutionService.execute(
    new HttpGet("http://www.google.com"), HttpClientContext.create(),
    new OkidokiHandler());
// blocks until the request complete and then returns true if you can connect to Google
boolean ok=task.get();
  • 取消任務

計劃任務可能會被取消。如果任務尚未執行,但只是排隊等待執行,它將永遠不會執行。如果正在執行並且mayInterruptIfRunning參數設置爲true,則會在請求上調用abort()否則響應將被忽略,但請求將被允許正常完成。對task.get()的任何後續調用都將失敗,並顯示IllegalStateException。應該注意的是取消任務只是釋放客戶端資源。該請求實際上可以在服務器端正常處理。

task.cancel(true)
task.get() // throws an Exception
  •  

回調

可以使用FutureCallback實例而不是手動調用task.get(),在請求完成時獲取回調。這與HttpAsyncClient中使用的接口相同。

private final class MyCallback implements FutureCallback<Boolean> {

    public void failed(final Exception ex) {
        // do something
    }

    public void completed(final Boolean result) {
        // do something
    }

    public void cancelled() {
        // do something
    }
}

HttpRequestFutureTask<Boolean> task = futureRequestExecutionService.execute(
    new HttpGet("http://www.google.com"), HttpClientContext.create(),
    new OkidokiHandler(), new MyCallback());

度量(metrics)

FutureRequestExecutionService通常用於進行大量Web服務調用的應用程序中。爲了便於例如監視或配置調整,FutureRequestExecutionService跟蹤幾個指標。每個HttpRequestFutureTask都提供了獲取任務計劃,啓動和結束時間的方法。另外,請求和任務持續時間也是可用的。這些指標彙總在FutureRequestExecutionMetrics實例中的FutureRequestExecutionService中,該實例可以通過FutureRequestExecutionService.metrics()進行訪問。

task.scheduledTime() // returns the timestamp the task was scheduled
task.startedTime() // returns the timestamp when the task was started
task.endedTime() // returns the timestamp when the task was done executing
task.requestDuration // returns the duration of the http request
task.taskDuration // returns the duration of the task from the moment it was scheduled

FutureRequestExecutionMetrics metrics = futureRequestExecutionService.metrics()
metrics.getActiveConnectionCount() // currently active connections
metrics.getScheduledConnectionCount(); // currently scheduled connections
metrics.getSuccessfulConnectionCount(); // total number of successful requests
metrics.getSuccessfulConnectionAverageDuration(); // average request duration
metrics.getFailedConnectionCount(); // total number of failed tasks
metrics.getFailedConnectionAverageDuration(); // average duration of failed tasks
metrics.getTaskCount(); // total number of tasks scheduled
metrics.getRequestCount(); // total number of requests
metrics.getRequestAverageDuration(); // average request duration
metrics.getTaskAverageDuration(); // average task duration

希望大家留言評論指正!
讓這篇教程譯文越來越標準,可讀性更強!幫助更多的人!
感謝!


[1]:官方教程原文鏈接 https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/index.html

發佈了48 篇原創文章 · 獲贊 42 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章