HttpClient 是 Apache Jakarta Common 下的子項目,能夠用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,而且它支持 HTTP 協議最新的版本和建議。本文首先介紹 HTTPClient,而後根據做者實際工做經驗給出了一些常見問題的解決方法。html
HttpClient簡介
HTTP 協議多是如今 Internet 上使用得最多、最重要的協議了,愈來愈多的 Java 應用程序須要直接經過 HTTP 協議來訪問網絡資源。雖然在 JDK 的 java.net 包中已經提供了訪問 HTTP 協議的基本功能,可是對於大部分應用程序來講,JDK 庫自己提供的功能還不夠豐富和靈活。HttpClient 是 Apache Jakarta Common 下的子項目,用來提供高效的、最新的、功能豐富的支持 HTTP 協議的客戶端編程工具包,而且它支持 HTTP 協議最新的版本和建議。HttpClient 已經應用在不少的項目中,好比 Apache Jakarta 上很著名的另外兩個開源項目 Cactus 和 HTMLUnit 都使用了 HttpClient,更多使用 HttpClient 的應用能夠參見http://wiki.apache.org/jakarta-httpclient/HttpClientPowered。HttpClient 項目很是活躍,使用的人仍是很是多的。目前 HttpClient 版本是在 2005.10.11 發佈的 3.0 RC4 。java
HttpClient 功能介紹
如下列出的是 HttpClient 提供的主要的功能,要知道更多詳細的功能能夠參見 HttpClient 的主頁。算法
- 實現了全部 HTTP 的方法(GET,POST,PUT,HEAD 等)
- 支持自動轉向
- 支持 HTTPS 協議
- 支持代理服務器等
下面將逐一介紹怎樣使用這些功能。首先,咱們必須安裝好 HttpClient。數據庫
HttpClient 基本功能的使用
GET 方法
使用 HttpClient 須要如下 6 個步驟:apache
1. 建立 HttpClient 的實例編程
2. 建立某種鏈接方法的實例,在這裏是 GetMethod。在 GetMethod 的構造函數中傳入待鏈接的地址json
3. 調用第一步中建立好的實例的 execute 方法來執行第二步中建立好的 method 實例api
4. 讀 response瀏覽器
5. 釋放鏈接。不管執行方法是否成功,都必須釋放鏈接
6. 對獲得後的內容進行處理
根據以上步驟,咱們來編寫用GET方法來取得某網頁內容的代碼。
- 大部分狀況下 HttpClient 默認的構造函數已經足夠使用。
HttpClient httpClient = new HttpClient();
- 建立GET方法的實例。在GET方法的構造函數中傳入待鏈接的地址便可。用GetMethod將會自動處理轉發過程,若是想要把自動處理轉發過程去掉的話,能夠調用方法setFollowRedirects(false)。
GetMethod getMethod = new GetMethod("http://www.ibm.com/");
- 調用實例httpClient的executeMethod方法來執行getMethod。因爲是執行在網絡上的程序,在運行executeMethod方法的時候,須要處理兩個異常,分別是HttpException和IOException。引發第一種異常的緣由主要多是在構造getMethod的時候傳入的協議不對,好比不當心將"http"寫成"htp",或者服務器端返回的內容不正常等,而且該異常發生是不可恢復的;第二種異常通常是因爲網絡緣由引發的異常,對於這種異常 (IOException),HttpClient會根據你指定的恢復策略自動試着從新執行executeMethod方法。HttpClient的恢復策略能夠自定義(經過實現接口HttpMethodRetryHandler來實現)。經過httpClient的方法setParameter設置你實現的恢復策略,本文中使用的是系統提供的默認恢復策略,該策略在碰到第二類異常的時候將自動重試3次。executeMethod返回值是一個整數,表示了執行該方法後服務器返回的狀態碼,該狀態碼能表示出該方法執行是否成功、須要認證或者頁面發生了跳轉(默認狀態下GetMethod的實例是自動處理跳轉的)等。
//設置成了默認的恢復策略,在發生異常時候將自動重試3次,在這裏你也能夠設置成自定義的恢復策略
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
//執行getMethod
int statusCode = client.executeMethod(getMethod);
if (statusCode != HttpStatus.SC_OK) {
System.err.println("Method failed: " + getMethod.getStatusLine());
}
- 在返回的狀態碼正確後,便可取得內容。取得目標地址的內容有三種方法:第一種,getResponseBody,該方法返回的是目標的二進制的byte流;第二種,getResponseBodyAsString,這個方法返回的是String類型,值得注意的是該方法返回的String的編碼是根據系統默認的編碼方式,因此返回的String值可能編碼類型有誤,在本文的"字符編碼"部分中將對此作詳細介紹;第三種,getResponseBodyAsStream,這個方法對於目標地址中有大量數據須要傳輸是最佳的。在這裏咱們使用了最簡單的getResponseBody方法。
byte[] responseBody = method.getResponseBody();
- 釋放鏈接。不管執行方法是否成功,都必須釋放鏈接。
method.releaseConnection();
- 處理內容。在這一步中根據你的須要處理內容,在例子中只是簡單的將內容打印到控制檯。
System.out.println(new String(responseBody));
下面是程序的完整代碼,這些代碼也可在附件中的test.GetSample中找到。
package com.httpclientTest;
/**
* HttpClient get方法功能介紹
* 實現了全部 HTTP 的方法(GET,POST,PUT,HEAD 等)
* 支持自動轉向
* 支持 HTTPS 協議
* 支持代理服務器等
*/
import java.io.IOException;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.http.protocol.HTTP;
/**
* @author 做者:wn
* @version 建立時間:2016年12月30日 上午10:48:25
*
*/
/**
* 調用實例httpClient的executeMethod方法來執行getMethod。
* 因爲是執行在網絡上的程序,在運行executeMethod方法的時候,須要處理兩個異常,
* 分別是HttpException和IOException。引發第一種異常的緣由主要多是在構
* 造getMethod的時候傳入的協議不對,好比不當心將"http"寫成"htp",或者服務
* 器端返回的內容不正常等,而且該異常發生是不可恢復的;第二種異常通常是因爲網絡緣由
* 引發的異常,對於這種異常 (IOException),HttpClient會根據你指定的恢復
* 策略自動試着從新執行executeMethod方法。HttpClient的恢復策略能夠自定義
* (經過實現接口HttpMethodRetryHandler來實現)。經過httpClient的方
* 法setParameter設置你實現的恢復策略,本文中使用的是系統提供的默認恢復策略,
* 該策略在碰到第二類異常的時候將自動重試3次。executeMethod返回值是一個整數,
* 表示了執行該方法後服務器返回的狀態碼,該狀態碼能表示出該方法執行是否成功、須要認
* 證或者頁面發生了跳轉(默認狀態下GetMethod的實例是自動處理跳轉的)等。
*
*
* 在返回的狀態碼正確後,便可取得內容。取得目標地址的內容有三種方法:
* 第一種,getResponseBody,該方法返回的是目標的二進制的byte流;
* 第二種,getResponseBodyAsString,這個方法返回的是String類型,值得注
* 意的是該方法返回的String的編碼是根據系統默認的編碼方式,因此返回的String值可
* 能編碼類型有誤,在本文的"字符編碼"部分中將對此作詳細介紹;
* 第三種,getResponseBodyAsStream,這個方法對於目標地址中有大量數據須要傳
* 輸是最佳的。在這裏咱們使用了最簡單的getResponseBody方法
*
*
* 在返回的狀態碼正確後,便可取得內容。取得目標地址的內容有三種方法:
* 第一種,getResponseBody,該方法返回的是目標的二進制的byte流;
* 第二種,getResponseBodyAsString,這個方法返回的是String類型,值得注意
* 的是該方法返回的String的編碼是根據系統默認的編碼方式,因此返回的String值可能編
* 碼類型有誤,在本文的"字符編碼"部分中將對此作詳細介紹;
* 第三種,getResponseBodyAsStream,這個方法對於目標地址中有大量數據須要傳輸
* 是最佳的。在這裏咱們使用了最簡單的getResponseBody方法。
*
*/
public class GetSample {
public static void main(String[] args) {
// 構造httpclient的實例
HttpClient httpclient = new HttpClient();
// 設置編碼模式
httpclient.getParams().setContentCharset("UTF-8");
// 建立get方法的實例
GetMethod getMethod = new GetMethod("http://www.baidu.com");
//GetMethod syncMethod = new GetMethod(baseUrl + syncUrl + "?start=" + dateStr + "&end=" + dateStr);
// 使用系統提供的默認的恢復策略
getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
new DefaultHttpMethodRetryHandler());
// 設置響應頭格式
// getMethod.addRequestHeader(HTTP.CONTENT_TYPE,"application/x-www-form-urlencoded;charset=utf-8");
// 添加響應頭
// String sign="";
// String nonce ="";
// getMethod.addRequestHeader("authorization", "sign=\"" + sign
// + "\"," + "nonce=\"" + nonce + "\"");
try {
// 執行getMethod
int statusCode = httpclient.executeMethod(getMethod);
if (statusCode != HttpStatus.SC_OK) {
System.err
.println("Method failed:" + getMethod.getStatusLine());
}
// 讀取內容
byte[] responseBody = getMethod.getResponseBody();
// 處理內容
System.out.println(new String(responseBody));
// String result = getMethod.getResponseBodyAsString();
} catch (HttpException e) {
// 發生致命的異常,多是協議不對或者返回的內容有問題
System.out.println("Please check your provided http address!");
e.printStackTrace();
} catch (IOException e) {
// 發生網絡異常
e.printStackTrace();
} finally {
// 釋放鏈接
getMethod.releaseConnection();
}
}
}
POST方法
根據RFC2616,對POST的解釋以下:POST方法用來向目的服務器發出請求,要求它接受被附在請求後的實體,並把它看成請求隊列(Request-Line)中請求URI所指定資源的附加新子項。POST被設計成用統一的方法實現下列功能:
- 對現有資源的註釋(Annotation of existing resources)
- 向電子公告欄、新聞組,郵件列表或相似討論組發送消息
- 提交數據塊,如將表單的結果提交給數據處理過程
- 經過附加操做來擴展數據庫
調用HttpClient中的PostMethod與GetMethod相似,除了設置PostMethod的實例與GetMethod有些不一樣以外,剩下的步驟都差很少。在下面的例子中,省去了與GetMethod相同的步驟,只說明與上面不一樣的地方,並以登陸清華大學BBS爲例子進行說明。
構造PostMethod以前的步驟都相同,與GetMethod同樣,構造PostMethod也須要一個URI參數,在本例中,登陸的地址是http://www.newsmth.net/bbslogin2.php。在建立了PostMethod的實例以後,須要給method實例填充表單的值,在BBS的登陸表單中須要有兩個域,第一個是用戶名(域名叫id),第二個是密碼(域名叫passwd)。表單中的域用類NameValuePair來表示,該類的構造函數第一個參數是域名,第二參數是該域的值;將表單全部的值設置到PostMethod中用方法setRequestBody。另外因爲BBS登陸成功後會轉向另一個頁面,可是HttpClient對於要求接受後繼服務的請求,好比POST和PUT,不支持自動轉發,所以須要本身對頁面轉向作處理。具體的頁面轉向處理請參見下面的"自動轉向"部分。代碼以下:
package com.httpclientTest;
import java.io.IOException;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
/**
* @author 做者:wn
* @version 建立時間:2016年12月30日 下午1:59:35
*
*/
/**
*
* 構造PostMethod以前的步驟都相同,與GetMethod同樣,構造PostMethod也須要一個URI參數,
* 在本例中,登陸的地址是http://www.newsmth.net/bbslogin2.php。在建立了PostMethod
* 的實例以後,須要給method實例填充表單的值,在BBS的登陸表單中須要有兩個域,第一個是用戶名(域名叫id)
* ,第二個是密碼(域名叫passwd)。表單中的域用類NameValuePair來表示,該類的構造函數第一個參數是
* 域名,第二參數是該域的值;將表單全部的值設置到PostMethod中用方法setRequestBody。另外因爲BBS
* 登陸成功後會轉向另一個頁面,可是HttpClient對於要求接受後繼服務的請求,好比POST和PUT,不支持自
* 動轉發,所以須要本身對頁面轉向作處理。具體的頁面轉向處理請參見下面的"自動轉向"部分。代碼以下:
*
*/
public class PostSamp {
public static void main(String[] args) {
// 構造httpclient的實例
HttpClient httpclient = new HttpClient();
// 設置編碼模式
httpclient.getParams().setContentCharset("UTF-8");
// 建立get方法的實例
String url = "http://www.newsmth.net/bbslogin2.php";
PostMethod postMethod = new PostMethod(url);
// 設置響應頭格式
// postMethod.addRequestHeader(HTTP.CONTENT_TYPE,"application/x-www-form-urlencoded;charset=utf-8");
// 添加響應頭
// String sign="";
// String nonce ="";
// postMethod.addRequestHeader("authorization", "sign=\"" + sign
// + "\"," + "nonce=\"" + nonce + "\"");
//填入各表單域的值
NameValuePair[] data = { new NameValuePair("id", "youUserName"),new NameValuePair("passwd", "yourPwd") };
// 將表單的值放入postMethod中
postMethod.setRequestBody(data);
// 執行postMethod
try {
// 執行postMethod
int statusCode = httpclient.executeMethod(postMethod);
// 檢查是否重定向
// HttpClient對於要求接受後繼服務的請求,象POST和PUT等不能自動處理轉發
// 301或者302
if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY ||
statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
// 從頭中取出轉向的地址
Header locationHeader = postMethod.getResponseHeader("location");
String location = null;
/**
* 自動轉向根據RFC2616中對自動轉向的定義,主要有兩種:301和302。301表示永久的移走(Moved Permanently,
* 當返回的是301,則表示請求的資源已經被移到一個固定的新地方,任何向該地址發起請求都會被轉到新的地址上。
* 302表示暫時的轉向,好比在服務器端的servlet程序調用了sendRedirect方法,則在客戶端就會獲得一個302的代碼,
* 這時服務器返回的頭信息中location的值就是sendRedirect轉向的目標地址。HttpClient支持自動轉向處理,可是
* 象POST和PUT方式這種要求接受後繼服務的請求方式,暫時不支持自動轉向,所以若是碰到POST方式提交後返回的是301或者
* 302的話須要本身處理。就像剛纔在POSTMethod中舉的例子:若是想進入登陸BBS後的頁面,必須從新發起登陸的請求,請求
* 的地址能夠在頭字段location中獲得。不過須要注意的是,有時候location返回的多是相對路徑,所以須要對location
* 返回的值作一些處理才能夠發起向新地址的請求。
*/
if (locationHeader != null) {
location = locationHeader.getValue();
System.out.println("The page was redirected to:" + location);
} else {
System.err.println("Location field value is null.");
}
// 讀取內容
byte[] responseBody = postMethod.getResponseBody();
// 處理內容
System.out.println(new String(responseBody));
// String result = postMethod.getResponseBodyAsString();
return;
}
} catch (HttpException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
// 釋放鏈接
postMethod.releaseConnection();
}
}
}
模擬輸入用戶名和口令進行登陸
本小節應該說是HTTP客戶端編程中最常遇見的問題,不少網站的內容都只是對註冊用戶可見的,這種狀況下就必需要求使用正確的用戶名和口令登陸成功後,方可瀏覽到想要的頁面。由於HTTP協議是無狀態的,也就是鏈接的有效期只限於當前請求,請求內容結束後鏈接就關閉了。在這種狀況下爲了保存用戶的登陸信息必須使用到Cookie機制。以JSP/Servlet爲例,當瀏覽器請求一個JSP或者是Servlet的頁面時,應用服務器會返回一個參數,名爲jsessionid(因不一樣應用服務器而異),值是一個較長的惟一字符串的Cookie,這個字符串值也就是當前訪問該站點的會話標識。瀏覽器在每訪問該站點的其餘頁面時候都要帶上jsessionid這樣的Cookie信息,應用服務器根據讀取這個會話標識來獲取對應的會話信息。
對於須要用戶登陸的網站,通常在用戶登陸成功後會將用戶資料保存在服務器的會話中,這樣當訪問到其餘的頁面時候,應用服務器根據瀏覽器送上的Cookie中讀取當前請求對應的會話標識以得到對應的會話信息,而後就能夠判斷用戶資料是否存在於會話信息中,若是存在則容許訪問頁面,不然跳轉到登陸頁面中要求用戶輸入賬號和口令進行登陸。這就是通常使用JSP開發網站在處理用戶登陸的比較通用的方法。
這樣一來,對於HTTP的客戶端來說,若是要訪問一個受保護的頁面時就必須模擬瀏覽器所作的工做,首先就是請求登陸頁面,而後讀取Cookie值;再次請求登陸頁面並加入登陸頁所需的每一個參數;最後就是請求最終所需的頁面。固然在除第一次請求外其餘的請求都須要附帶上Cookie信息以便服務器能判斷當前請求是否已經經過驗證。說了這麼多,但是若是你使用httpclient的話,你甚至連一行代碼都無需增長,你只須要先傳遞登陸信息執行登陸過程,而後直接訪問想要的頁面,跟訪問一個普通的頁面沒有任何區別,由於類HttpClient已經幫你作了全部該作的事情了,太棒了!下面的例子實現了這樣一個訪問的過程。
/*
* Created on 2003-12-7 by Liudong
*/
package com.zuidaima.http.demo;
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.cookie.*;
import org.apache.commons.httpclient.methods.*;
/**
* 用來演示登陸表單的示例
* @author Liudong
*/
public class FormLoginDemo {
static final String LOGON_SITE = "localhost" ;
static final int LOGON_PORT = 8080;
public static void main(String[] args) throws Exception{
HttpClient client = new HttpClient();
client.getHostConfiguration().setHost(LOGON_SITE, LOGON_PORT);
// 模擬登陸頁面 login.jsp->main.jsp
PostMethod post = new PostMethod( "/main.jsp" );
NameValuePair name = new NameValuePair( "name" , "ld" );
NameValuePair pass = new NameValuePair( "password" , "ld" );
post.setRequestBody( new NameValuePair[]{name,pass});
int status = client.executeMethod(post);
System.out.println(post.getResponseBodyAsString());
post.releaseConnection();
// 查看 cookie 信息
CookieSpec cookiespec = CookiePolicy.getDefaultSpec();
Cookie[] cookies = cookiespec.match(LOGON_SITE, LOGON_PORT, "/" , false , client.getState().getCookies());
if (cookies.length == 0) {
System.out.println( "None" );
} else {
for ( int i = 0; i < cookies.length; i++) {
System.out.println(cookies[i].toString());
}
}
// 訪問所需的頁面 main2.jsp
GetMethodget=newGetMethod("/main2.jsp");
client.executeMethod(get);
System.out.println(get.getResponseBodyAsString());
get.releaseConnection();
}
}
提交XML格式參數
提交XML格式的參數很簡單,僅僅是一個提交時候的ContentType問題,下面的例子演示從文件文件中讀取XML信息並提交給服務器的過程,該過程能夠用來測試Web服務。
package com.zuidaima.httpclient;
import java.io.File;
import java.io.FileInputStream;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.PostMethod;
/**
*用來演示提交XML格式數據的例子
*/
public class PostXMLClient {
public static void main(String[] args) throws Exception {
File input = new File(「test.xml」);
PostMethod post = new PostMethod(「http://localhost:8080/httpclient/xml.jsp」);
// 設置請求的內容直接從文件中讀取
post.setRequestBody( new FileInputStream(input));
if (input.length() < Integer.MAX_VALUE)
post.setRequestContentLength(input.length());
else
post.setRequestContentLength(EntityEnclosingMethod.CONTENT_LENGTH_CHUNKED);
// 指定請求內容的類型
post.setRequestHeader( "Content-type" , "text/xml; charset=GBK" );
HttpClient httpclient = new HttpClient();
int result = httpclient.executeMethod(post);
System.out.println( "Response status code: " + result);
System.out.println( "Response body: " );
System.out.println(post.getResponseBodyAsString());
post.releaseConnection();
}
}
經過HTTP上傳文件
httpclient使用了單獨的一個HttpMethod子類來處理文件的上傳,這個類就是MultipartPostMethod,該類已經封裝了文件上傳的細節,咱們要作的僅僅是告訴它咱們要上傳文件的全路徑便可,下面的代碼片斷演示如何使用這個類。
MultipartPostMethod filePost = new MultipartPostMethod(targetURL);
filePost.addParameter( "fileName" , targetFilePath);
HttpClient client = new HttpClient();
// 因爲要上傳的文件可能比較大 , 所以在此設置最大的鏈接超時時間
client.getHttpConnectionManager(). getParams().setConnectionTimeout(5000);
int status = client.executeMethod(filePost);
上面代碼中,targetFilePath即爲要上傳的文件所在的路徑
訪問啓用認證的頁面
咱們常常會碰到這樣的頁面,當訪問它的時候會彈出一個瀏覽器的對話框要求輸入用戶名和密碼後方可,這種用戶認證的方式不一樣於咱們在前面介紹的基於表單的用戶身份驗證。這是HTTP的認證策略,httpclient支持三種認證方式包括:基本、摘要以及NTLM認證。其中基本認證最簡單、通用但也最不安全;摘要認證是在HTTP 1.1中加入的認證方式,而NTLM則是微軟公司定義的而不是通用的規範,最新版本的NTLM是比摘要認證還要安全的一種方式。
下面例子是從httpclient的CVS服務器中下載的,它簡單演示如何訪問一個認證保護的頁面
package com.zuidaima.httpclient;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.methods.GetMethod;
public class BasicAuthenticationExample {
public BasicAuthenticationExample() {
}
public static void main(String[] args) throws Exception {
HttpClient client = new HttpClient();
client.getState().setCredentials( "www.verisign.com" , "realm" , new UsernamePasswordCredentials( "username" , "password") );
GetMethod get = new GetMethod( "https://www.verisign.com/products/index.html" );
get.setDoAuthentication( true );
int status = client.executeMethod( get );
System.out.println(status+ "\n" + get.getResponseBodyAsString());
get.releaseConnection();
}
}
多線程模式下使用httpclient
多線程同時訪問httpclient,例如同時從一個站點上下載多個文件。對於同一個HttpConnection同一個時間只能有一個線程訪問,爲了保證多線程工做環境下不產生衝突,httpclient使用了一個多線程鏈接管理器的類:MultiThreadedHttpConnectionManager,要使用這個類很簡單,只須要在構造HttpClient實例的時候傳入便可,代碼以下:
MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
HttpClient client = new HttpClient(connectionManager);
之後儘管訪問client實例便可。
使用HttpClient過程當中常見的一些問題
下面介紹在使用HttpClient過程當中常見的一些問題。
字符編碼
某目標頁的編碼可能出如今兩個地方,第一個地方是服務器返回的http頭中,另一個地方是獲得的html/xml頁面中。
- 在http頭的Content-Type字段可能會包含字符編碼信息。例如可能返回的頭會包含這樣子的信息:Content-Type: text/html; charset=UTF-8。這個頭信息代表該頁的編碼是UTF-8,可是服務器返回的頭信息未必與內容能匹配上。好比對於一些雙字節語言國家,可能服務器返回的編碼類型是UTF-8,但真正的內容卻不是UTF-8編碼的,所以須要在另外的地方去獲得頁面的編碼信息;可是若是服務器返回的編碼不是UTF-8,而是具體的一些編碼,好比gb2312等,那服務器返回的多是正確的編碼信息。經過method對象的getResponseCharSet()方法就能夠獲得http頭中的編碼信息。
- 對於象xml或者html這樣的文件,容許做者在頁面中直接指定編碼類型。好比在html中會有<meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>這樣的標籤;或者在xml中會有<?xml version="1.0" encoding="gb2312"?>這樣的標籤,在這些狀況下,可能與http頭中返回的編碼信息衝突,須要用戶本身判斷到底那種編碼類型應該是真正的編碼。
自動轉向
根據RFC2616中對自動轉向的定義,主要有兩種:301和302。301表示永久的移走(Moved Permanently),當返回的是301,則表示請求的資源已經被移到一個固定的新地方,任何向該地址發起請求都會被轉到新的地址上。302表示暫時的轉向,好比在服務器端的servlet程序調用了sendRedirect方法,則在客戶端就會獲得一個302的代碼,這時服務器返回的頭信息中location的值就是sendRedirect轉向的目標地址。
HttpClient支持自動轉向處理,可是象POST和PUT方式這種要求接受後繼服務的請求方式,暫時不支持自動轉向,所以若是碰到POST方式提交後返回的是301或者302的話須要本身處理。就像剛纔在POSTMethod中舉的例子:若是想進入登陸BBS後的頁面,必須從新發起登陸的請求,請求的地址能夠在頭字段location中獲得。不過須要注意的是,有時候location返回的多是相對路徑,所以須要對location返回的值作一些處理才能夠發起向新地址的請求。
另外除了在頭中包含的信息可能使頁面發生重定向外,在頁面中也有可能會發生頁面的重定向。引發頁面自動轉發的標籤是:<meta http-equiv="refresh" content="5; url=http://www.ibm.com/us">。若是你想在程序中也處理這種狀況的話得本身分析頁面來實現轉向。須要注意的是,在上面那個標籤中url的值也能夠是一個相對地址,若是是這樣的話,須要對它作一些處理後才能夠轉發。
處理HTTPS協議
HttpClient提供了對SSL的支持,在使用SSL以前必須安裝JSSE。在Sun提供的1.4之後的版本中,JSSE已經集成到JDK中,若是你使用的是JDK1.4之前的版本則必須安裝JSSE。JSSE不一樣的廠家有不一樣的實現。下面介紹怎麼使用HttpClient來打開Https鏈接。這裏有兩種方法能夠打開https鏈接,第一種就是獲得服務器頒發的證書,而後導入到本地的keystore中;另一種辦法就是經過擴展HttpClient的類來實現自動接受證書。
一個https協議的例子:
package com.fenxiao.channel.youpaiyun;
import java.io.IOException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.sevenstar.component.cache.ehcache.EHCacheHelper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.fenxiao.channel.youpaiyun.util.CryptAES;
/**
* @author 做者:wn
* @version 建立時間:2016年12月22日 下午3:55:33
*
*/
public class YouPaiYunTest {
@SuppressWarnings("unused")
private Logger log = LogManager.getLogger(YouPaiYunTest.class);
String get_token_url = "https://ptp-api.upyun.com/refreshToken";
String flowurl = "https://ptp-api.upyun.com/chargeOrder";
String appkey = "UOdaq2g9jR2t9wBS";
String appsecret = "d1a464de8590118d693a8859536b92a0";
String queryurl = "https://ptp-api.upyun.com/seekOrder";
public static void main(String[] args) throws Exception {
YouPaiYunTest test = new YouPaiYunTest();
test.chargeOrder();
//test.seekOrder();
}
/**
* 建立訂單
* @throws Exception
*/
public void chargeOrder() throws Exception {
System.out.println("=====可當下單到又拍雲====");
// 獲取token,token有效期120分鐘
String token = refreshToken(get_token_url, appkey, appsecret);
if (StringUtils.isEmpty(token)) {
System.out.println("=====又拍雲獲取token異常====");
return;
}else {
System.out.println("=====youpaiyun_token===token:"+token);
}
//手機號
String mobile0 = "18902506499";
String mobile = CryptAES.encrypt(mobile0, token, appkey);
//流量包的產品編號
String prodcode = "CTC_5"; //電信5M,1元
//客戶自定義的訂單號,用於客戶內部記錄訂單,長度必須小於30, 且每次充值,編號不重複
String custno = "15415451115127";
//簽名
String sign ="";
//拼接字符串
String content = new StringBuffer().append("appkey").append(appkey).append("custno").append(custno).append("mobile").append(mobile).append("prodcode").append(prodcode).append("token").append(token).toString();
sign = CryptAES.SHA1(content);
System.out.println("=======簽名:"+sign);
//String result = sendPost(flowurl, sign);
JSONObject json = new JSONObject();
json.put("appkey", appkey);
json.put("custno", custno);
json.put("mobile", mobile);
json.put("prodcode", prodcode);
json.put("sign", sign);
try {
String fUrl = flowurl +json.toString();
System.out.println(fUrl);
String params = json.toString();
String retJSON = HttpRequestUtil.sendHttpsRequest(flowurl, params);
System.out.println("retJSON====" + retJSON);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 查詢seekOrder
*/
public Object seekOrder()throws HttpException,IOException{
String custno = "15415451115126";
String requesttime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date());
String sign="";
System.out.println("=====開始查詢又拍雲====");
// 獲取token,token有效期120分鐘
String token = refreshToken(get_token_url, appkey, appsecret);
if (StringUtils.isEmpty(token)) {
System.out.println("=====又拍雲獲取token異常====");
return null;
}else {
System.out.println("=====youpaiyun_token===token:"+token);
}
//拼接字符串
String content = new StringBuffer().append("appkey").append(appkey).append("custno").
append(custno).append("requesttime").append(requesttime).append("TOKEN").append(token).toString();
System.out.println("=======拼接好的字符串:"+content);
sign = CryptAES.SHA1(content);
System.out.println("=======簽名:"+sign);
JSONObject json = new JSONObject();
json.put("appkey", appkey);
json.put("custno", custno);
json.put("requesttime", requesttime);
json.put("sign", sign);
String fUrl = queryurl +json.toString();
System.out.println(fUrl);
String params = json.toString();
String retJSON = HttpRequestUtil.sendHttpsRequest(queryurl, params);
System.out.println("retJSON====" + retJSON);
return retJSON;
}
/**
* 令牌請求接口
*/
@SuppressWarnings("unchecked")
public static String refreshToken(String url,String appkey,String appsecret){
String token = "";
Map<String, Object> map = null;
//從緩存中獲取token
Object cacheToken = EHCacheHelper.get("youpanyun_token", "YOUPAIYUN_TOKEN");
if (null!=cacheToken) {
token = String.valueOf(cacheToken);
}
System.out.println("===從緩存中獲取youpaiyun_token=====cacheToken:"+token);
if (StringUtils.isBlank(token)) {
System.out.println("====從緩存中沒法獲取youpaiyun_token,則從新獲取又拍雲Token====");
JSONObject json = new JSONObject();
json.put("appkey", appkey);
json.put("appsecret", appsecret);
String param = json.toString();
System.out.println("====請求參數===="+param);
try {
param = URLDecoder.decode(param, "UTF-8");
System.out.println(url+param);
String response = HttpRequestUtil.sendHttpsRequest(url, param);
map = (Map<String, Object>) JSON.parse(response);
System.out.println("=====獲取又拍雲token的結果response:"+ response);
} catch (Exception e) {
e.printStackTrace();
}
if(null != map && map.containsKey("token")){
token = (String) map.get("token");
System.out.println("=====建立新的youpaiyun_token===token:" + token);
//儲存token
storageToken("YOUPAIYUN_TOKEN",token);
}else{
System.out.println("===又拍雲獲取token異常===returnMap:null");
}
storageToken("YPUPAIYUN_TOKEN",token);
}
return token;
}
/**
* 存儲 token
*
* @param key
* @param value
*/
public static void storageToken(String key, String value) {
if (!EHCacheHelper.exists("YOUPAIYUN_TOKEN")) {
System.out.println("===EHCache not found [YOUPAIYUN_TOKEN]===");
EHCacheHelper.addCache("YOUPAIYUN_TOKEN");
EHCacheHelper.getCache("YOUPAIYUN_TOKEN").getCacheConfiguration().setTimeToIdleSeconds(0);// 無限期地處於空閒狀態
EHCacheHelper.getCache("YOUPAIYUN_TOKEN").getCacheConfiguration().setTimeToLiveSeconds(7100);// 容許存在於緩存中的最長時間,以秒爲單位.超過600s自動從緩存中清除
EHCacheHelper.getCache("YOUPAIYUN_TOKEN").getCacheConfiguration().setMaxElementsInMemory(100);// 設置基於內存的緩存可存放對象的最大數目
EHCacheHelper.getCache("YOUPAIYUN_TOKEN").getCacheConfiguration().setDiskPersistent(false);
EHCacheHelper.getCache("YOUPAIYUN_TOKEN").getCacheConfiguration().setEternal(false);
}
EHCacheHelper.put("youpanyun_token", key, value);
if (null != EHCacheHelper.get("youpanyun_token","YOUPAIYUN_TOKEN")) {
System.out.println("===已存儲YOUPAIYUN_TOKEN===TOKEN:"+ EHCacheHelper.get("youpanyun_token","YOUPAIYUN_TOKEN") + "");
}
}
}
View Code
Https協議工具類:
package com.fenxiao.channel.youpaiyun;
import javax.net.ssl.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Created by Administrator on 2014/11/11.
*/
public class HttpRequestUtil {
private static String encoding = "utf-8";
/**
* http方式調用
* @param remoteUrl
* @param message
* @return
*/
public static String sendHttpRequest(String remoteUrl, String message) {
URLConnection conn = null;
try {
URL url = new URL(remoteUrl);
conn = url.openConnection();
//發送請求
if (conn instanceof HttpURLConnection) {
HttpURLConnection httpUrlConnection = (HttpURLConnection) conn;
httpUrlConnection.setRequestMethod("POST");
byte[] data = message.getBytes(encoding);
httpUrlConnection.setConnectTimeout(20 * 1000); //20s超時
httpUrlConnection.setReadTimeout(20 * 1000);//20s
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setUseCaches(false);
httpUrlConnection.setInstanceFollowRedirects(false);
httpUrlConnection.addRequestProperty("Content-Type", "application/json;charset=" + encoding);
httpUrlConnection.addRequestProperty("Content-Length", String.valueOf(data.length));
OutputStream out = httpUrlConnection.getOutputStream();
out.write(data);
// flush and close
out.flush();
out.close();
}
//響應返回
InputStreamReader inputStreamReader = new InputStreamReader(conn.getInputStream(), encoding);
BufferedReader in = new BufferedReader(inputStreamReader);
String line = null;
final StringBuilder stringBuffer = new StringBuilder(255);
while ((line = in.readLine()) != null) {
stringBuffer.append(line);
stringBuffer.append("\n");
}
String responseMessage = stringBuffer.toString();
return responseMessage;
} catch (final Exception e) {
e.printStackTrace();
} finally {
if (conn != null && conn instanceof HttpURLConnection) {
((HttpURLConnection) conn).disconnect();
}
}
return null;
}
/**
* https方式調用
* @param remoteUrl
* @param message
* @return
*/
public static String sendHttpsRequest(String remoteUrl, String message) {
URLConnection conn = null;
try {
URL url = new URL(remoteUrl);
//信任與主機驗證
trustAllHosts();
conn = url.openConnection();
//發送請求
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection httpUrlConnection = (HttpsURLConnection) conn;
httpUrlConnection.setRequestMethod("POST");
byte[] data = message.getBytes(encoding);
httpUrlConnection.setConnectTimeout(20 * 1000); //20s超時
httpUrlConnection.setReadTimeout(20 * 1000);//20s
httpUrlConnection.setDoOutput(true);
httpUrlConnection.setUseCaches(false);
httpUrlConnection.setInstanceFollowRedirects(false);
httpUrlConnection.addRequestProperty("Content-Type", "application/json;charset=" + encoding);
httpUrlConnection.addRequestProperty("Content-Length", String.valueOf(data.length));
OutputStream out = httpUrlConnection.getOutputStream();
out.write(data);
// flush and close
out.flush();
out.close();
}
//響應返回
InputStreamReader inputStreamReader = new InputStreamReader(conn.getInputStream(), encoding);
BufferedReader in = new BufferedReader(inputStreamReader);
String line = null;
final StringBuilder stringBuffer = new StringBuilder(255);
while ((line = in.readLine()) != null) {
stringBuffer.append(line);
stringBuffer.append("\n");
}
String responseMessage = stringBuffer.toString();
return responseMessage;
} catch (final Exception e) {
e.printStackTrace();
} finally {
if (conn != null && conn instanceof HttpURLConnection) {
((HttpURLConnection) conn).disconnect();
}
}
return null;
}
/**
* @param encoding the encoding to set
*/
public void setEncoding(String encoding) {
HttpRequestUtil.encoding = encoding;
}
/**
*建立一個信任管理器不驗證證書鏈及主機名的驗證
*/
public static void trustAllHosts() {
//建立一個信任管理器不驗證證書鏈
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[] {};
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
}
}
};
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
//若是不建立新的信任管理器,則須要服務器端的keystore文件,才能訪問服務端
// String sslKeyStorePath = "C:\\deploy\\jetty.keystore";
// String sslKeyStorePassword = "12345678";
// String sslKeyStoreType = "JKS"; // 密鑰庫類型,有JKS PKCS12等
// String sslTrustStore = "C:\\deploy\\jetty.keystore";
// String sslTrustStorePassword = "12345678";
// System.setProperty("javax.net.ssl.keyStore", sslKeyStorePath);
// System.setProperty("javax.net.ssl.keyStorePassword",
// sslKeyStorePassword);
// System.setProperty("javax.net.ssl.keyStoreType", sslKeyStoreType);
// // 設置系統參數
// System.setProperty("javax.net.ssl.trustStore", sslTrustStore);
// System.setProperty("javax.net.ssl.trustStorePassword",
// sslTrustStorePassword);
// System.setProperty("java.protocol.handler.pkgs", "sun.net.www.protocol");
//主機名的驗證,返回true
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String urlHostName, SSLSession session) {
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hv);
} catch (Exception e) {
e.printStackTrace();
}
}
}
View Code
方法1,取得證書,並導入本地的keystore:
- 安裝JSSE (若是你使用的JDK版本是1.4或者1.4以上就能夠跳過這一步)。本文以IBM的JSSE爲例子說明。先到IBM網站上下載JSSE的安裝包。而後解壓開以後將ibmjsse.jar包拷貝到<java-home>\lib\ext\目錄下。
- 取得而且導入證書。證書能夠經過IE來得到:
1. 用IE打開須要鏈接的https網址,會彈出以下對話框:
2. 單擊"View Certificate",在彈出的對話框中選擇"Details",而後再單擊"Copy to File",根據提供的嚮導生成待訪問網頁的證書文件
3. 嚮導第一步,歡迎界面,直接單擊"Next",
4. 嚮導第二步,選擇導出的文件格式,默認,單擊"Next",
5. 嚮導第三步,輸入導出的文件名,輸入後,單擊"Next",
6. 嚮導第四步,單擊"Finish",完成嚮導
7. 最後彈出一個對話框,顯示導出成功
-
用keytool工具把剛纔導出的證書倒入本地keystore。Keytool命令在<java-home>\bin\下,打開命令行窗口,併到<java-home>\lib\security\目錄下,運行下面的命令:
keytool -import -noprompt -keystore cacerts
-storepass changeit -alias yourEntry1 -file your.cer
其中參數alias後跟的值是當前證書在keystore中的惟一標識符,可是大小寫不區分;參數file後跟的是剛纔經過IE導出的證書所在的路徑和文件名;若是你想刪除剛纔導入到keystore的證書,能夠用命令:
keytool -delete -keystore cacerts -storepass changeit -alias yourEntry1
- 寫程序訪問https地址。若是想測試是否能連上https,只須要稍改一下GetSample例子,把請求的目標變成一個https地址。
GetMethod getMethod = new GetMethod("https://www.yourdomain.com");
運行該程序可能出現的問題:
1. 拋出異常java.net.SocketException: Algorithm SSL not available。出現這個異常多是由於沒有加JSSEProvider,若是用的是IBM的JSSE Provider,在程序中加入這樣的一行:
if(Security.getProvider("com.ibm.jsse.IBMJSSEProvider") == null)
Security.addProvider(new IBMJSSEProvider());
或者也能夠打開<java-home>\lib\security\java.security,在行
security.provider.1=sun.security.provider.Sun
security.provider.2=com.ibm.crypto.provider.IBMJCE
後面加入security.provider.3=com.ibm.jsse.IBMJSSEProvider
2. 拋出異常java.net.SocketException: SSL implementation not available。出現這個異常多是你沒有把ibmjsse.jar拷貝到<java-home>\lib\ext\目錄下。
3. 拋出異常javax.net.ssl.SSLHandshakeException: unknown certificate。出現這個異常代表你的JSSE應該已經安裝正確,可是可能由於你沒有把證書導入到當前運行JRE的keystore中,請按照前面介紹的步驟來導入你的證書。
方法2,擴展HttpClient類實現自動接受證書
由於這種方法自動接收全部證書,所以存在必定的安全問題,因此在使用這種方法前請仔細考慮您的系統的安全需求。具體的步驟以下:
- 提供一個自定義的socket factory(test.MySecureProtocolSocketFactory)。這個自定義的類必須實現接口org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory,在實現接口的類中調用自定義的X509TrustManager(test.MyX509TrustManager),這兩個類能夠在隨本文帶的附件中獲得
- 建立一個org.apache.commons.httpclient.protocol.Protocol的實例,指定協議名稱和默認的端口號
Protocol myhttps = new Protocol("https", new MySecureProtocolSocketFactory (), 443);
- 註冊剛纔建立的https協議對象
Protocol.registerProtocol("https ", myhttps);
- 而後按照普通編程方式打開https的目標地址,代碼請參見test.NoCertificationHttpsGetSample
處理代理服務器
HttpClient中使用代理服務器很是簡單,調用HttpClient中setProxy方法就能夠,方法的第一個參數是代理服務器地址,第二個參數是端口號。另外HttpClient也支持SOCKS代理。
httpClient.getHostConfiguration().setProxy(hostName,port);
結論
從上面的介紹中,能夠知道HttpClient對http協議支持很是好,使用起來很簡單,版本更新快,功能也很強大,具備足夠的靈活性和擴展性。對於想在Java應用中直接訪問http資源的編程人員來講,HttpClient是一個不可多得的好工具。