1.2 HttpClient 接口
HttpClient
接口代表着HTTP请求执行的最基本的契约。它对请求的执行没有什么限制与特定的细节,并且对于连接管理、状态管理、验证和重定向的处理,是让各自的实现来决定。这让它更容易对接口进行装饰,添加额外的功能,如对响应内容进行缓存。
通常,HttpClient
实现是当作一个门面,可以包含一系列特殊目的的处理器或者策略接口,如重定向或验证处理,或决定连接持久性和保活时长的策略接口。这使用户能够选择性地替换对应的默认策略。
@Test
public void chapter1_2_0() {
ConnectionKeepAliveStrategy keepAliveStrategy = 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(keepAliveStrategy)
.build();
}
1.2.1 HttpClient 线程安全
HttpClient
实现是线程安全的。对于多个请求的执行,推荐使用同一个实例。
1.2.2 HttpClient 资源释放
当一个CloseableHttpClient
实例不再需要并且将超出连接管理器的作用范围时,它必须调用方法CloseableHttpClient#close()
来关闭。
public void chapter1_2_2() throws IOException {
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
// ...
} finally {
httpclient.close();
}
}
1.3 HTTP 执行上下文
最初HTTP被设计成一个无状态的,面向请求-响应的协议。然而,真实世界的应用程序经常需要在几个逻辑关联的请求响应交换中保持状态信息。为了能够让应用程序能够维持状态,HttpClient允许Http请求在一个特定的执行上下文中执行,该上下文被称为HTTP上下文(HTTP context)。当相同的上下文在连续的请求中重用时,几个逻辑关联的请求就会参与一个逻辑的会话。HTTP上下文类似于java.util.Map
。它就是一个简单键值对的集合。应用程序能够在执行前获取上下文,与能够在执行后检查上下文。
HttpContext
能够包含任意对象,所以在多个线程中共享可能是不安全的。推荐每个线程维护自己的上下文。
在HTTP请求的执行过程中,HttpClient会添加以下属性到挂靠上下文中:
-
HttpConnection
实例,表示与目标服务器的实际连接。 -
HttpHost
实例,表示连接的目标。 -
HttpRoute
实例,表示完整的连接路由。 -
HttpRequest
实例,表示实际的HTTP请求。由final修饰的HttpRequest对象总是能正确表示报文的状态,因为它是被送往目标服务器。对于缺省的HTTP/1.0和HTTP/1.1,它使用相对路径的请求URI。然而,如果请求是通过一个非隧道模式的代理来发送的话,URI是绝对路径的。 -
HttpResponse
实例,表示实际的HTTP响应。 -
java.lang.Boolean
对象,表示的标志,指明实际的请求是否被完全传送到连接目标。 -
ReqeustConfig
对象,表示实际请求的配置。 -
java.util.List
对象,表示在执行过程中收到的所有的重定向位置的集合。
我们可以使用HttpClientContext
适配器来简化与上下文状态的交互过程。
public void chapter1_3() {
HttpContext context = null; // init ...
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig requestConfig = clientContext.getRequestConfig();
}
一组逻辑关联的请求应当在相同的HttpContext
实例中执行,以保证在多个请求中自动传递会话上下文和状态信息。
在下面的例子中,由初始请求设置的请求配置会保存在执行上下文中,并通过共享相同的上下文来传递给接下来的请求。
public void chapter1_3_a() throws IOException {
CloseableHttpClient httpclient = HttpClients.createDefault();
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(1000)
.setConnectTimeout(1000)
.build();
HttpContext context = new BasicHttpContext();
HttpGet httpGet1 = new HttpGet("http://www.baidu.com");
httpGet1.setConfig(requestConfig);
CloseableHttpResponse response1 = httpclient.execute(httpGet1, context);
try {
HttpEntity entity1 = response1.getEntity();
} finally {
response1.close();
}
HttpGet httpGet2 = new HttpGet("http://www.yy.com");
CloseableHttpResponse response2 = httpclient.execute(httpGet2, context);
try {
HttpEntity entity2 = response2.getEntity();
} finally {
response2.close();
}
}
1.4 HTTP 协议拦截器
HTTP协议拦截器是一个实现了HTTP协议一个特定方面的例程。通常,协议拦截器期望作用于输入报文的一个特定的头部或一组相关的头部,或给输出报文填入一个特定的头部或一组相关的头部。协议拦截器也能够操作报文中的内容实体,透明的内容压缩/解压缩就是一个很好的例子。通常使用装饰者模式来实现,用包装的实体来装饰原始的实体。多个协议拦截器能够组合起来形成一个逻辑单元。
协议拦截器在HTTP执行上下文中通过共享信息来协作,如处理状态。协议拦截器能够使用HTTP上下文来保存一个请求或几个连续请求的处理状态。
通常拦截器的顺序应该是无关紧要的,只要它们不依赖执行上下文中的特定的状态。如果协议接口器存在依赖必须按一定的顺序执行,它们应该按照该顺序添加到协议处理器中。
协议接口器必须实现为线程安全的。与servlet类似,协议拦截器不应该使用实例变量,除非访问那些实例变量是同步的。
以下的例子展示的本地上下文在连续的请求中是如何被使用来保存处理状态的:
public void chapter1_4() throws IOException {
CloseableHttpClient httpclient = HttpClients.custom()
.addInterceptorLast(new HttpRequestInterceptor() {
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
AtomicInteger count = (AtomicInteger) httpContext.getAttribute("count");
httpRequest.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();
}
}
}
# 1.5 异常处理
HTTP协议处理器会抛出种类型的异常:java.io.IOException
(I/O错误如socket超时或者socket重置)和HttpException
(表示HTTP错误如违反HTTP协议)。 通常I/O错误可以认为是非致命的和可恢复的,而HTTP协议错误是致命的且不能自动恢复的。请注意HttpClient
的实现把HttpException
重新抛出为ClientProtocolException
,它是java.io.IOException
的子类。这使得用户可以在一个catch子句中处理I/O错误和协议错误。
1.5.1 HTTP 传输安全
理解HTTP协议不是对所有应用程序都适用是很重要的。HTTP是一个简单的面向请求/响应的协议,它最初被设计成支持静态或动态产生的内容的获取。它从未打算支持事务操作。例如,HTTP服务器成功地接收并处理请求,产生一个响应并发送状态码给回客户端,那么服务器就会认为它成功了。如果客户端因为读超时、取消请求或系统崩溃,服务器不会试图回滚这个事务。如果客户端决定进行重试相同的请求,那么服务器不可避免地会重复执行一次相同的请求。在某种情况下,这会导致数据损坏或程序状态不一致。
尽管HTTP从未被设计成支持事务处理,它仍然能够被用作关键任务的传输协议,如果特定的条件满足的话。为了保证HTTP传输层安全,系统必须保证在应用层上的HTTP方法的幂等性。
1.5.2 幂等方法
HTTP/1.1规格中定义一个幂等方法为:
Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects os N > 0 identical requests is the same as for a single request.
也就是说,应用程序应当确保它能够多次执行相同方法而不产生副作用。提供一个唯一的事务ID或用其他方法避免执行相同逻辑操作就能达到这一目的。
请注意到这不仅限于HttpClient。基于浏览器的应用程序一样有相同的问题。
HttpClient默认认为只有非包含实体的方法,如GET
和HEAD
是幂等的,包含实体的方法如POST
和PUT
,基于兼容性原因,是非幂等的。
1.5.3 自动的异常恢复
HttpClient默认会自动地从I/O异常中恢复。缺省的自动恢复机制仅适用于一些已知是安全的异常。
- HttpClient不会试图从任务逻辑错误或HTTP协议错误(那些继续
HttpException
的类)中自动恢复。 - HttpClient会自动重试那些认为是幂等的方法。
- HttpClient 会自动重试那些有传输异常的方法,如果该HTTP请求仍然在传输给目标服务器(例如没有完全传输到服务的请求)。
1.5.4 请求重试处理器
为了实现自定义的恢复机制,我们可以提供一个HttprequestRetryHandler
接口的实现。
public void chapter1_5_4() {
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 ConnectTimeoutException) {
// Connection refused
return false;
}
if (exception instanceof InterruptedIOException) {
// Timeout
return false;
}
if (exception instanceof UnknownHostException) {
// Unknown host
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
。
1.6 中止请求
在某些情况下,HTTP请求在期望的时间内未执行成功,可能是因为目标服务器的高负载或在客户端有过多的并发请求。这种情况下,可能需要过早的结束请求并释放被I/O操作阻塞的线程。任何被HttpClient执行的HTTP请求能够在任何阶段通过调用HttpUriRequest#abort()
方法来中止。这个方法是线程安全的且能被任何线程调用。当一个HTTP请求被中止,它的执行线程(即使被I/O操作阻塞)也能够保证解锁(抛出异常InterruptedIOException
)。
1.7 重定向处理
HttpClient自动处理所有类型的重定向,除了那些被HTTP规约显示禁止的需要用户干预的。参考其他在POST
和PUT
上的重定向(状态码303),HTTP规约要求其转换成GET
请求。我们可以使用一个自定义的重定向策略来放宽HTTP规约对POST方法的限制。
public void chapter1_7_a() {
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
.setRedirectStrategy(redirectStrategy)
.build();
}
HttpClient经常在执行的过程中重写请求报文。对于默认的HTTP/1.0和HTTP/1.1来说通常是使用相对路径的请求URI。同样地,原始请求可能被多次重定向到另一个位置上。最终被解析的绝对路径的HTTP地址,能够使用原始请求和上下文来构建。工具方法URIUtils#resolve
就是用来构建最终解析到的绝对路径URI,从而生成最终的请求。这个方法包含重定向请求中的最后一个段的标识或原始请求。
@Test
public void chapter1_7_b() throws IOException, URISyntaxException {
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpGet = new HttpGet("http://www.yy.com");
CloseableHttpResponse response = httpclient.execute(httpGet, context);
try {
HttpHost target = context.getTargetHost();
List redirectLocation = context.getRedirectLocations();
URI location = URIUtils.resolve(httpGet.getURI(), target, redirectLocation);
System.out.println("Final Http location: " + location.toASCIIString());
} finally {
response.close();
}
}