1.2 HttpClient 接口
HttpClient接口代表HTTP request执行的最基本的协议。它不规定任何request执行过程中的限制或者细节,并且将连接管理,状态管理,认证和重定向处理交给各自独立实现,这使得它更容易去使用附加功能去装饰接口,如response content cacheing。
通常HttpClient的实现作为一系列特定的handler或者strategy接口的入口,而这些特定的handler或者strategy各自负责HTTP协议处理的一个方面,比如重定向或者认证处理,或者连接持久化和保活周期处理。这使得用户可以有选择的替代这些方面默认实现。
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();
1.2.1 HttpClient线程安全
HttpClient的实现是线程安全的,建议不同的request 执行过程用同一个该类的实例。
1.2.2 HttpClient资源释放
当CloseableHttpClient不再使用并且将离开其所关联的连接管理器的范围时,其必须通过调用CloseableHttpClient#close方法关闭。
CloseableHttpClient httpclient = HttpClients.createDefault(); try { <...> } finally { httpclient.close(); }
1.3 HTTP运行期上下文
最初HTTP被设计为无状态的,面向response-request的协议。但是现实世界中应用程序经常需要通过一些逻辑相关的request-response交换来保持状态信息。为了让应用程序能够保持处理状态,HttpClient允许HTTP requests在一个特别的运行期上下文中执行,即HTTP context。多数连续的逻辑相关的request如果使用相同的context,那么他们可以分享同一个逻辑session,HTTP context有点像java.util.Map<String,Object>,它只是简单的手机任意形式的name-value。应用程序可以在request执行之前填充context或者市在执行之后检查context。
HttpContext可以包含任意的对象,因此在不同的线程之间共享是不安全的,建议每个线程保存其自己的Context。
在HTTP request请求的过程中,HttpClient将会添加如下属性到运行上下文中。
HttpConnection 实例代表着到目标服务器的真实连接。
HttpHost实例代表着连接的目标
HttpRoute实例代表着完整的连接路由
HttpRequest 实例代表着实际的HTTP request请求,因为HttpRequest是发送到目标服务器中的,所以它最终在执行上下文中的状态即表示着这条消息的状态。一般来说HTTP/1.0和HTTP、1.1的request都默认使用相对路径,但是如果request是通过代理服务器,而且工作在非隧道模式下,那么这时就必须使用绝对路径了。
HttpResponse 实例代表着实际的HTTP response
java.lang.Boolean 对象标识着request是否完全发送到了目标服务器
RequestConfig 对象表示实际的request配置
java.util.List<URI> 对象表示着在执行request过程中受到的所有重定向请求。
你可以使用HttpClientContext适配器类简化与context的交互操作。
HttpContext context = <...> HttpClientContext clientContext = HttpClientContext.adapt(context); HttpHost target = clientContext.getTargetHost(); HttpRequest request = clientContext.getRequest(); HttpResponse response = clientContext.getResponse(); RequestConfig config = clientContext.getRequestConfig();
多个逻辑相关,依赖于session的request应该在同样的HttpContext上下文环境中执行以保证会话上下文和状态能够在不同的request中传递。
在下面的例子中,request初始化时创建了request configuration配置,此配置会保存在执行上下文中并且在连续不同的request之间传递用以分享同样的上下文。
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(); }
1.4 HTTP protocol 拦截器
HTTP protocol拦截器是实现了HTTP protocol某些特定方面的一段程序。通常,protocol拦截器会对入栈消息的某一个或者一组指定的headers起作用,或者用某一个或者一组特定的headers填充出栈消息。protocol拦截器也可以操作封装好内容实体的消息,比如进行内容压缩或者解压缩,通常我们会通过‘Decorator’模式来完成这些操作,特别是在使用实体包装器类来封装源实体时。多个protocol拦截器可以组合形成一个逻辑性的单元。
Protocol拦截器可以通过共享信息(比如HTTP执行上下文中的处理状态)来进行合作,Protocol拦截器也可以通过HTTPContext来存储一个或者一系列request的处理状态。
由于拦截器不依赖于执行环境中的某个特定的状态,所以他们的执行顺序并不重要,但是如果你对protocol的执行顺序有要求,那么他们应该按照执行的顺序添加到protocol processor中去。
Procotol必须是线程安全的,跟Servlets类似,protocol拦截器不应该使用实例变量,除非这些实例变量是同步的。
下面是一个local context在连续的request中存储处理状态的例子。
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(); } }
1.5. Exception 处理
HTTP protocol processors 会抛出两种异常:
java.io.IOException 在I/O失败时,如socket超时或者socket重置。
HttpException 标识HTTP 失败,如违反HTTP协议。
通常 I/O 失败是非致命并且可以恢复的,而HTTP protocol错误是致命的并且不可自动恢复的。请注意HttpClient的实现类将ClientProtocolException当做HttpException抛出,ClientProtocolException是java.io.IOException的子类,这使得用户在一个catch里可以同时处理I/O错误和协议错误。
1.5.1 HTTP 安全传输
要明白HTTP 协议并不是适合所有的应用。HTTP 是一个简单的request/response协议,它在最初的设计用于静态/动态内容检索,它从来没有试图去支持事务操作。举个例子来说,HTTP 服务器在接收并且处理request时只会考虑完成自己负责的这一部分协议的工作,生成response和发送状态码给客户端,如果客户端接收失败(读取超时,请求取消或者系统崩溃),服务器不会尝试去回滚这个操作,如果服务器支持事务,那么当客户端决定重试请求,服务器将不止一次的强制终止执行相同的事务,而这样会导致应用的数据损坏或者状态不一致。
即使HTTP从来没有考虑过支持事务处理,在某些特定的情况下它也可以作为关键应用的传输协议,为了保证HTTP传输层的安全,使用系统必须在应用层确保HTTP 方法的幂等性。
1.5.2 方法幂等性
HTTP/1.1 规范将幂等性做如下定义:
[ 方法有幂等性即表示N>0个相同请求的副作用跟单一请求的副作用一样 ]
换句话来说就表示应用应该确保已准备好处理执行多个相同方法带来的影响,而这是有办法可以实现的,比如提供一个唯一的事务id,意味着避免执行相同的逻辑操作。
请注意这个问题并不是专门针对于HttpClient,浏览器都会受到相同问题的影响。
基于兼容性的原因,HttpClient假设只有非实体封装的方法,比如GET 和HEAD是幂等性的,而实体封装方法如POST和PUT不是幂等性的。
1.5.3 异常自动恢复
HttpClient默认会自动尝试从I/O异常中恢复,自动恢复机制只作用于一些已经被认为是安全的异常:
HttpClient不会尝试从任何逻辑或者HTTP protocol错误中恢复(HttpException的衍生类)。
HttpClient会自动重试那些被认为是幂等性的方法。
HttpClient会自动重试在Http request发送到目标服务器时出现传输失败的方法。
1.5.4 Request retry handler
为了启用自定义的错误恢复机制,你应该提供一个HttpRequestRetryHandler接口的实现。
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();
请注意你可以使用StandardHttpRequestRetryHandler来替换默认实现,从而实现RFC-2616中定义的幂等性方法的自动重试(GET,HEAD,PUT,DELETE,OPTIONS,AND TRACE)。
1.6 终止请求
在某些情况下,HTTP request在期望的时间内无法执行完成,导致目标服务器的高负载或者客户端存在许多同步的请求,从而你会需要去提前中断request请求并且将阻塞在I/O操作的执行线程释放出来,在HttpClient中执行的request可以在执行过程中的任何阶段通过调用HttpUriRequest#abort()终止,这个方法是线程安全的并且可以从任何线程起调。当HTTP request在执行线程中终止时,会抛出InterruptedIOException。
1.7 Redirect Handling
HttpClient自动处理所有类型的重定向,除了在HTTP规范中明确禁止并且要求用户干预的。在HTTP规范中,通过POST和PUT请求的303 redirect会被转换成GET请求。你可以使用自定义重定向策略去放宽HTTP规范中关于POST方法重定向的限制。
LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy(); CloseableHttpClient httpclient = HttpClients.custom() .setRedirectStrategy(redirectStrategy) .build();
HttpClient在其执行过程中经常需要重写request消息。HTTP/1.0和HTTP/1.1经常使用相对请求,同样的,原始请求可能会被重定向多次,最终确定的HTTP路径可以通过原始request和context组合出来,URIUtils#resolve实用方法可以用来明确的最终请求的URI路径。这个方法包括重定向请求或者原始request的最后一个片段标识符。
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(); }