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是一个不可多得的好工具。