前言
舊版本的HttpClient已經停止維護了,它已經被Apache HttpComponents項目的HttpClient和HttpCore模塊替代。Hyper-Text Transfer Protocol (HTTP)也許是當今互聯網上使用的最爲重要的協議。雖然java.net package提供了基本的從HTTP獲取資源的功能,但是它不提供全面的靈活性或許多應用程序所需的功能。HttpClient試圖填補這一空白,通過提供一個有效的、最新的、功能豐富的方案實現客戶端最近的HTTP標準和建議。注意HttpClient不是瀏覽器,而是一個客戶端HTTP傳輸庫,目的是傳送和接收HTTP信息,它缺少瀏覽器需要的UI, HTML渲染器和JavaScript引擎。可以直接到官網下載,或者使用maven配置。HttpClient 4.5 需要Java 1.5或者更新的版本。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
HttpClient最基本的功能是執行HTTP方法,執行一個HTTP方法包含了一個或者幾個的HTTP REQUEST/HTTP RESPONSE交換,這通常在HttpClient內部處理。用戶提供一個被執行的請求對象,HttpClient傳送這個請求到目標服務器並且返回一個對應的響應對象,或者當請求不成功時拋出一個異常。所有的HTTP請求都有一行,它包含了請求請求方法名、請求URI和HTTP協議版本(1.0或者1.1)。
請求
HttpClient支持開箱即用的,所有定義在HTTP/1.1規範中的方法:GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS。每個方法類型都有一個指定的類:HttpGet、HttpHead、HttpPost、HttpPut、HttpDelete、HttpTrace、HttpOptions。
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響應是服務端在接收和處理客戶端消息後返回給客戶端的消息。消息的第一行包含了協議版本,緊接着是狀態碼和狀態碼描述文字。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
System.out.println(response.getProtocolVersion());//HTTP/1.1
System.out.println(response.getStatusLine().getStatusCode());//200
System.out.println(response.getStatusLine().getReasonPhrase());//OK
System.out.println(response.getStatusLine().toString());//HTTP/1.1 200 OK
消息頭
HTTP消息可以包含一些用來描述消息屬性的頭部消息,例如content length, 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);//Set-Cookie: c1=a; path=/; domain=localhost
Header h2 = response.getLastHeader("Set-Cookie");
System.out.println(h2);//Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
Header[] hs = response.getHeaders("Set-Cookie");
System.out.println(hs.length);//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());
}
st
同樣,它也提供了將頭部消息解析成單個元素的方法
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實體
HTTP消息可以包含內容實體,HTTP定義了兩個實體封裝請求方法:PUT和POST。HttpClient依靠內容的來源來區分三種實體。
streamed:內容來源於流或者動態生成,特別是,包含從HTTP響應接收的實體,streamed實體一般不可重複生成的。
self-contained:內容位於內存中或者是可獲得的,意味着它是獨立於連接和其他實體的,Self-contained實體一般可重複,這種類型的實體大都用於HTTP請求的封裝。
wrapping:內容來源於其他實體。
對於連接管理來說,當從HTTP響應中用流輸出內容的時候這些區分的重要的。對於僅僅由應用程序創建並且用HttpClient發送的請求實體來說,streamed和self-contained的區別是不重要的。既然如此,那麼就認爲不可重複的實體是streamed,可重複的實體是self-contained。
可重複的實體,表示它的內容可以不止一次被讀取,例如ByteArrayEntity和StringEntity。爲了讀取內容,任何人都可以使用HttpEntity#getContent()返回java.io.InputStream,或者用HttpEntity#writeTo(OutputStream)提供給輸出流。
當實體通過一個收到的報文獲取時,HttpEntity#getContentType()方法和HttpEntity#getContentLength()方法可以用來讀取通用的元數據,如Content-Type和Content-Length頭部信息(如果它們是可用的)。因爲頭部信息Content-Type可以包含對文本MIME類型的字符編碼,比如text/plain或text/html,HttpEntity#getContentEncoding()方法用來讀取這個信息。如果頭部信息Content-Length不可用,那麼就返回長度-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();
}
有些情況下,僅僅response的一小部分需要被取回並且消耗內容的剩餘部分且保持連接可用的性能代價是很高的,這種情況下可以直接關閉response。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();
}
消耗實體內容
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);
}
生成實體內容
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);
HTML表單
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);
內容分塊
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);
response處理
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的接口
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資源分配
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
<...>
} finally {
httpclient.close();
}
Http執行上下文
HttpContext可以包含任意類型的對象,因此如果在多線程中共享上下文會不安全。建議每個線程都只包含自己的http上下文。
在Http請求執行的過程中,HttpClient會自動添加下面的屬性到Http上下文中:
HttpConnection的實例,表示客戶端與服務器之間的連接
HttpHost的實例,表示要連接的目標服務器
HttpRoute的實例,表示全部的連接路由
HttpRequest的實例,表示Http請求。在執行上下文中,最終的HttpRequest對象會代表http消息的狀態。Http/1.0和Http/1.1都默認使用相對的uri。但是如果使用了非隧道模式的代理服務器,就會使用絕對路徑的uri。
HttpResponse的實例,表示Http響應
java.lang.Boolean對象,表示是否請求被成功的發送給目標服務器
RequestConfig對象,表示http request的配置信息
java.util.List<Uri>對象,表示Http響應中的所有重定向地址
可以使用HttpClientContext這個適配器來簡化和上下文狀態交互的過程。
HttpContext context = <...>
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();
同一個邏輯會話中的多個Http請求,應該使用相同的Http上下文來執行,這樣就可以自動地在http請求中傳遞會話上下文和狀態信息。在下面的例子中,我們在開頭設置的參數,會被保存在上下文中,並且會應用到後續的http請求中。
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協議攔截器
下面是個例子,講述了本地的上下文時如何在連續請求中記錄處理狀態的:
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傳輸安全
方法的冪等性
異常自動修復
HttpClient不會嘗試修復任何邏輯或者http協議錯誤(即從HttpException衍生出來的異常)。
HttpClient會自動再次發送冪等的方法(如果首次執行失敗)。
HttpClient會自動再次發送遇到transport異常的方法,前提是Http請求仍舊保持着連接(例如http請求沒有全部發送給目標服務器,HttpClient會再次嘗試發送)。
請求重試HANDLER
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();
中斷請求
重定向處理
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
HttpClient在請求執行過程中,經常需要重寫請求的消息。 HTTP/1.0和HTTP/1.1都默認使用相對的uri路徑。同樣,原始的請求可能會被者多次的重定向。最終絕對路徑可以使用原始的請求和上下文來構建。URIUtils#resolve可以用於構建絕對路徑,產生最終的請求。這個方法包含了最後一個分片標識符或者原始請求。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();
}