(第一章下)
1.1.7.2HTML表单
许多应用程序需要频繁模拟提交一个HTML表单的过程,比如,为了来记录一个Web应用程序或提交输出数据。HttpClient提供了特殊的实体类UrlEncodedFormEntity来这个满足过程。
List<NameValuePair>formparams=newArrayList<NameValuePair>();
formparams.add(newBasicNameValuePair("param1","value1"));
formparams.add(newBasicNameValuePair("param2","value2"));
UrlEncodedFormEntityentity=newUrlEncodedFormEntity(formparams,"UTF-8");
HttpPosthttppost=newHttpPost("http://localhost/handler.do");
httppost.setEntity(entity);
UrlEncodedFormEntity实例将会使用URL编码来编码参数,生成如下的内容:
param1=value1?m2=value2
1.1.7.3内容分块
通常,我们推荐让HttpClient选择基于被传递的HTTP报文属性的最适合的编码转换。这是可能的,但是,设置HttpEntity#setChunked()方法为true是通知HttpClient分块编码的首选。请注意HttpClient将会使用标识作为提示。当使用的HTTP协议版本,如HTTP/1.0版本,不支持分块编码时,这个值会被忽略。
StringEntityentity=newStringEntity("importantmessage",
"text/plain;charset=\"UTF-8\"");
entity.setChunked(true);
HttpPosthttppost=newHttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);
1.1.8响应控制器
控制响应的最简便和最方便的方式是使用ResponseHandler接口。这个放完完全减轻了用户关于连接管理的担心。当使用ResponseHandler时,HttpClient将会自动关注并保证释放连接到连接管理器中去,而不管请求执行是否成功或引发了异常。
HttpClienthttpclient=newDefaultHttpClient();
HttpGethttpget=newHttpGet("http://localhost/");
ResponseHandler<byte[]>handler=newResponseHandler<byte[]>(){
publicbyte[]handleResponse(HttpResponseresponse)throwsClientProtocolException,IOException{
HttpEntityentity=response.getEntity();
if(entity!=null){
returnEntityUtils.toByteArray(entity);
}else{
returnnull;
}
}
};
byte[]response=httpclient.execute(httpget,handler);
1.2HTTP执行的环境
最初,HTTP是被设计成无状态的,面向请求-响应的协议。然而,真实的应用程序经常需要通过一些逻辑相关的请求-响应交换来持久状态信息。为了开启应用程序来维持一个过程状态,HttpClient允许HTTP请求在一个特定的执行环境中来执行,简称为HTTP上下文。如果相同的环境在连续请求之间重用,那么多种逻辑相关的请求可以参与到一个逻辑会话中。HTTP上下文功能和java.util.Map<String,Object>很相似。它仅仅是任意命名参数值的集合。应用程序可以在请求之前或在检查上下文执行完成之后来填充上下文属性。
在HTTP请求执行的这一过程中,HttpClient添加了下列属性到执行上下文中:
'http.connection' :HttpConnection实例代表了连接到目标服务器的真实连接。
'http.target_host' :HttpHost实例代表了连接目标。
'http.proxy_host' :如果使用了,HttpHost实例代表了代理连接。
'http.request' :HttpRequest实例代表了真实的HTTP请求。
'http.response' :HttpResponse实例代表了真实的HTTP响应。
'http.request_sent' :java.lang.Boolean对象代表了暗示真实请求是否被完全传送到目标连接的标识。
比如,为了决定最终的重定向目标,在请求执行之后,可以检查http.target_host属性的值:
DefaultHttpClienthttpclient=newDefaultHttpClient();
HttpContextlocalContext=newBasicHttpContext();
HttpGethttpget=newHttpGet("http://www.google.com/");
HttpResponseresponse=httpclient.execute(httpget,localContext);
HttpHosttarget=(HttpHost)localContext.getAttribute(
ExecutionContext.HTTP_TARGET_HOST);
System.out.println("Finaltarget:"+target);
HttpEntityentity=response.getEntity();
if(entity!=null){
entity.consumeContent();
}
输出内容为:
Finaltarget:http://www.google.ch
1.3异常处理
HttpClient能够抛出两种类型的异常:在I/O失败时,如套接字连接超时或被重置的java.io.IOException异常,还有标志HTTP请求失败的信号,如违反HTTP协议的HttpException异常。通常I/O错误被认为是非致命的和可以恢复的,而HTTP协议错误则被认为是致命的而且是不能自动恢复的。
1.3.1HTTP运输安全
要理解HTTP协议并不是对所有类型的应用程序都适合的,这一点很重要。HTTP是一个简单的面向请求/响应的协议,最初被设计用来支持取回静态或动态生成的内容。它从未向支持事务性操作方向发展。比如,如果成功收到和处理请求,HTTP服务器将会考虑它的其中一部分是否完成,生成一个响应并发送一个状态码到客户端。如果客户端因为读取超时,请求取消或系统崩溃导致接收响应实体失败时,服务器不会试图回滚事务。如果客户端决定重新这个请求,那么服务器将不可避免地不止一次执行这个相同的事务。在一些情况下,这会导致应用数据损坏或者不一致的应用程序状态。
尽管HTTP从来都没有被设计来支持事务性处理,但它也能被用作于一个传输协议对关键的任务应用提供被满足的确定状态。要保证HTTP传输层的安全,系统必须保证HTTP方法在应用层的幂等性。
1.3.2幂等的方法
HTTP/1.1明确地定义了幂等的方法,描述如下
[方法也可以有“幂等”属性在那些(除了错误或过期问题)N的副作用>0的相同请求和独立的请求是相同的]
换句话说,应用程序应该保证准备着来处理多个相同方法执行的实现。这是可以达到的,比如,通过提供一个独立的事务ID和其它避免执行相同逻辑操作的方法。
请注意这个问题对于HttpClient是不具体的。基于应用的浏览器特别受和非幂等的HTTP方法相关的相同问题的限制。
HttpClient假设没有实体包含方法,比如GET和HEAD是幂等的,而实体包含方法,比如POST和PUT则不是。
1.3.3异常自动恢复
默认情况下,HttpClient会试图自动从I/O异常中恢复。默认的自动恢复机制是受很少一部分已知的异常是安全的这个限制。
HttpClient不会从任意逻辑或HTTP协议错误(那些是从HttpException类中派生出的)中恢复的。
HttpClient将会自动重新执行那么假设是幂等的方法。
HttpClient将会自动重新执行那些由于运输异常失败,而HTTP请求仍然被传送到目标服务器(也就是请求没有完全被送到服务器)失败的方法。
HttpClient将会自动重新执行那些已经完全被送到服务器,但是服务器使用HTTP状态码(服务器仅仅丢掉连接而不会发回任何东西)响应时失败的方法。在这种情况下,假设请求没有被服务器处理,而应用程序的状态也没有改变。如果这个假设可能对于你应用程序的目标Web服务器来说不正确,那么就强烈建议提供一个自定义的异常处理器。
1.3.4请求重试处理
为了开启自定义异常恢复机制,应该提供一个HttpRequestRetryHandler接口的实现。
DefaultHttpClienthttpclient=newDefaultHttpClient();
HttpRequestRetryHandlermyRetryHandler=newHttpRequestRetryHandler(){
publicbooleanretryRequest(IOExceptionexception,
intexecutionCount,HttpContextcontext){
if(executionCount>=5){
//如果超过最大重试次数,那么就不要继续了
returnfalse;
}
if(exceptioninstanceofNoHttpResponseException){
//如果服务器丢掉了连接,那么就重试
returntrue;
}
if(exceptioninstanceofSSLHandshakeException){
//不要重试SSL握手异常
returnfalse;
}
HttpRequestrequest=(HttpRequest)context.getAttribute(
ExecutionContext.HTTP_REQUEST);
booleanidempotent=!(requestinstanceofHttpEntityEnclosingRequest);
if(idempotent){
//如果请求被认为是幂等的,那么就重试
returntrue;
}
returnfalse;
}
};
httpclient.setHttpRequestRetryHandler(myRetryHandler);
1.4中止请求
在一些情况下,由于目标服务器的高负载或客户端有很多活动的请求,那么HTTP请求执行会在预期的时间框内而失败。这时,就可能不得不过早地中止请求,解除封锁在I/O执行中的线程封锁。被HttpClient执行的HTTP请求可以在执行的任意阶段通过调用HttpUriRequest#abort()方法而中止。这个方法是线程安全的,而且可以从任意线程中调用。当一个HTTP请求被中止时,它的执行线程就封锁在I/O操作中了,而且保证通过抛出InterruptedIOException异常来解锁。
1.5HTTP协议拦截器
HTTP协议拦截器是一个实现了特定HTPP协议方面的惯例。通常协议拦截器希望作用于一个特定头部信息上,或者一族收到报文的相关头部信息,或使用一个特定的头部或一族相关的头部信息填充发出的报文。协议拦截器也可以操纵包含在报文中的内容实体,透明的内容压缩/解压就是一个很好的示例。通常情况下这是由包装器实体类使用了“装饰者”模式来装饰原始的实体完成的。一些协议拦截器可以从一个逻辑单元中来结合。
协议拦截器也可以通过共享信息来共同合作-比如处理状态-通过HTTP执行上下文。协议拦截器可以使用HTTP内容来为一个或多个连续的请求存储一个处理状态。
通常拦截器执行的顺序不应该和它们基于的特定执行上下文状态有关。如果协议拦截器有相互依存关系,那么它们必须按特定顺序来执行,正如它们希望执行的顺序一样,它们应该在相同的序列中被加到协议处理器。
协议拦截器必须实现为线程安全的。和Servlet相似,协议拦截器不应该使用实例变量,除非访问的那些变量是同步的。
这个示例给出了本地内容在连续的请求中怎么被用于持久一个处理状态的:
DefaultHttpClienthttpclient=newDefaultHttpClient();
HttpContextlocalContext=newBasicHttpContext();
AtomicIntegercount=newAtomicInteger(1);
localContext.setAttribute("count",count);
httpclient.addRequestInterceptor(newHttpRequestInterceptor(){
publicvoidprocess(finalHttpRequestrequest,
finalHttpContextcontext)throwsHttpException,IOException{
AtomicIntegercount=(AtomicInteger)context.getAttribute("count");
request.addHeader("Count",Integer.toString(count.getAndIncrement()));
}
});
HttpGethttpget=newHttpGet("http://localhost/");
for(inti=0;i<10;i++){
HttpResponseresponse=httpclient.execute(httpget,localContext);
HttpEntityentity=response.getEntity();
if(entity!=null){
entity.consumeContent();
}
}
1.6HTTP参数
HttpParams接口代表了定义组件运行时行为的一个不变的值的集合。很多情况下,HttpParams和HttpContext相似。二者之间的主要区别是它们在运行时使用的不同。这两个接口表示了对象的集合,它们被视作为访问对象值的键的Map,但是服务于不同的目的:
HttpParams旨在包含简单对象:整型,浮点型,字符串,集合,还有运行时不变的对象。
HttpParams希望被用在“一次写入-多处准备”模式下。HttpContext旨在包含很可能在HTTP报文处理这一过程中发生改变的复杂对象
HttpParams的目标是定义其它组件的行为。通常每一个复杂的组件都有它自己的HttpParams对象。HttpContext的目标是来表示一个HTTP处理的执行状态。通常相同的执行上下文在很多合作的对象中共享。
1.6.1参数层次
在HTTP请求执行过程中,HttpRequest对象的HttpParams是和用于执行请求的HttpClient实例的HttpParams联系在一起的。这使得设置在HTTP请求级别的参数优先于设置在HTTP客户端级别的HttpParams。推荐的做法是设置普通参数对所有的在HTTP客户端级别的HTTP请求共享,而且可以选择性重写具体在HTTP请求级别的参数。
DefaultHttpClienthttpclient=newDefaultHttpClient();
httpclient.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_0);
httpclient.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET,"UTF-8");
HttpGethttpget=newHttpGet("http://www.google.com/");
httpget.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION,HttpVersion.HTTP_1_1);
httpget.getParams().setParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE,Boolean.FALSE);
httpclient.addRequestInterceptor(newHttpRequestInterceptor(){
publicvoidprocess(finalHttpRequestrequest,
finalHttpContextcontext)throwsHttpException,IOException{
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.PROTOCOL_VERSION));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.HTTP_CONTENT_CHARSET));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.USE_EXPECT_CONTINUE));
System.out.println(request.getParams().getParameter(
CoreProtocolPNames.STRICT_TRANSFER_ENCODING));
}
});
输出内容为:
HTTP/1.1
UTF-8
false
Null
1.6.2HTTP参数bean
HttpParams接口允许在处理组件的配置上很大的灵活性。很重要的是,新的参数可以被引入而不会影响老版本的二进制兼容性。然而,和常规的Javabean相比,HttpParams也有一个缺点:HttpParams不能使用DI框架来组合。为了缓解这个限制,HttpClient包含了一些bean类,它们可以用来按顺序使用标准的Javaeban惯例初始化HttpParams对象。
HttpParamsparams=newBasicHttpParams();
HttpProtocolParamBeanparamsBean=newHttpProtocolParamBean(params);
paramsBean.setVersion(HttpVersion.HTTP_1_1);
paramsBean.setContentCharset("UTF-8");
paramsBean.setUseExpectContinue(true);
System.out.println(params.getParameter(
CoreProtocolPNames.PROTOCOL_VERSION));
System.out.println(params.getParameter(
CoreProtocolPNames.HTTP_CONTENT_CHARSET));
System.out.println(params.getParameter(
CoreProtocolPNames.USE_EXPECT_CONTINUE));
System.out.println(params.getParameter(
CoreProtocolPNames.USER_AGENT));
输出内容为:
HTTP/1.1
UTF-8
false
Null
1.7HTTP请求执行参数
这些参数会影响到请求执行的过程:
'http.protocol.version':如果没有在请求对象中设置明确的版本信息,它就定义了使用的HTTP协议版本。这个参数期望得到一个ProtocolVersion类型的值。如果这个参数没有被设置,那么就使用HTTP/1.1。
'http.protocol.element-charset':定义了编码HTTP协议元素的字符集。这个参数期望得到一个java.lang.String类型的值。如果这个参数没有被设置,那么就使用US-ASCII。
'http.protocol.eontent-charset':定义了为每个内容主体编码的默认字符集。这个参数期望得到一个java.lang.String类型的值。如果这个参数没有被设置,那么就使用ISO-8859-1。
'http.useragent':定义了头部信息User-Agent的内容。这个参数期望得到一个java.lang.String类型的值。如果这个参数没有被设置,那么HttpClient将会为它自动生成一个值。
'http.protocol.strict-transfer-encoding':定义了响应头部信息中是否含有一个非法的Transfer-Encoding,都要拒绝掉。
'http.protocol.expect-continue':为包含方法的实体激活Expect:100-Continue握手。Expect:100-Continue握手的目的是允许客户端使用请求体发送一个请求信息来决定源服务器是否希望在客户端发送请求体之前得到这个请求(基于请求头部信息)。Expect:100-Continue握手的使用可以对需要目标服务器认证的包含请求的实体(比如POST和PUT)导致明显的性能改善。Expect:100-Continue握手应该谨慎使用,因为它和HTTP服务器,不支持HTTP/1.1协议的代理使用会引起问题。这个参数期望得到一个java.lang.Boolean类型的值。如果这个参数没有被设置,那么HttpClient将会试图使用握手。
'http.protocol.wait-for-continue':定义了客户端应该等待100-Continue响应最大的毫秒级时间间隔。这个参数期望得到一个java.lang.Integer类型的值。如果这个参数没有被设置,那么HttpClient将会在恢复请求体传输之前为确认等待3秒。