HttpComponents使用纪要

概览

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/15/

HttpComponents是Apache基金会开发和维护的一组底层HTTP及其它协议相关的Java套件包。

HttpComponents的前身是Apache Commons包下的HttpClient包,在3.1版本后,独立成了单独的一组套件,包全名为org.apache.httpcomponents,目前HttpCore的最新版本是4.4.11,HttpClient的最新版本是4.5.7。

鉴于在网络上搜索到的教程大多数都已经过时,多数基于老版本的HttpClient,或者老版本的HttpComponents,而官方的文档教程又是英文,艰涩难懂而且组织混乱。故总结了这篇小小的纪要,仅作入门使用。

个人原因:我曾经为一个基于Python的爬虫程序写过一个Java的GUI,当Java调用Python代码时过程极为复杂繁琐,而且充满了各种不兼容,故萌生出了用Java写爬虫的想法。而HttpComponents在Java中就像requests/urllib在Python中的地位一样。

项目结构

HttpComponents主要分为三个部分:Core、Client与AsyncClient。

HttpCore是一组低层次的HTTP传输组件,可用于构建自定义的客户端和服务器端服务。HttpCore支持两种I/O模型:基于传统Java I/O的阻塞I/O模型和基于Java NIO的非阻塞事件驱动I/O模型。

HttpClient是一个遵从HTTP/1.1协议的、基于HttpCore的一个HTTP代理实现类。它为客户端验证、HTTP状态管理和HTTP连接管理提供了可复用的组件。

AsyncClient是一个遵从HTTP/1.1协议的、基于HttpCore NIO和HttpClient的一个HTTP代理实现类。它与HttpClient是互补的,旨在应对处理高并发的能力要比原始数据吞吐量性能远远重要的场景。

由于仅仅是入门以及简单应用,我们只看HttpClient。

HttpClient的官方英文教程可在这里找到:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/ 。JavaDoc API可在这里找到:https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/ 。

请求处理

HttpClient最重要的功能是处理HTTP方法。处理HTTP方法的过程包括一个或多个HTTP请求(request)或HTTP相应(response)的交换,这个过程通常在HttpClient内部处理。用户(开发者)只需要提供一个请求对象,HttpClient就会将这个请求发往指定的服务器,并接收相应的相应对象,或者在处理不成功时抛出异常。

以下是一组简单的代码:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    <...>
} finally {
    response.close();
}

HTTP请求

所有的HTTP请求都有一个请求行,请求行是由方法、请求URI和HTTP版本号组成。

HttpClient支持所有HTTP/1.1协议规定的方法:GETHEADPOSTPUTDELETETRACEOPTIONS,每一个方法对应着一个类:HttpGetHttpHeadHttpPostHttpPutHttpDeleteHttpTraceHttpOptions。通常我们只使用get和post就足够了。

请求URI全称为Uniform Resource Identifier,即统一资源标识符,区别于URL。HTTP请求URI由一个协议、主机名称、端口(可选)、资源路径和参数(可选)组成。

HttpGet httpget = new HttpGet("http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");

以上构造HttpGet的方法也可以使用HttpClient提供的URIBuilder类来组装。

URI uri = new URIBuilder()
        .setScheme("http")
        .setHost("www.google.com")
        .setPath("/search")
        .setParameter("q", "httpclient")
        .setParameter("btnG", "Google Search")
        .setParameter("aq", "f")
        .setParameter("oq", "")
        .build();
HttpGet httpget = new HttpGet(uri);
System.out.println(httpget.getURI());

输出:

http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=

HTTP响应

HTTP响应是由服务器返回给客户端的信息。响应信息的第一行由协议版本、一个数字状态码和相关的文本信息组成。

使用HttpClient可以很轻易地组装出一个HTTP响应。

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, 
HttpStatus.SC_OK, "OK");

System.out.println(response.getProtocolVersion());
System.out.println(response.getStatusLine().getStatusCode());
System.out.println(response.getStatusLine().getReasonPhrase());
System.out.println(response.getStatusLine().toString());

输出:

HTTP/1.1
200
OK
HTTP/1.1 200 OK

信息头(头部信息)

一条HTTP消息可以包括许多头部信息,这些信息描述了消息的属性,例如消息长度、消息类型之类的。HttpClient提供了许多方法来操作头部信息。以下以一条响应消息为例:

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");
Header h1 = response.getFirstHeader("Set-Cookie");
System.out.println(h1);
Header h2 = response.getLastHeader("Set-Cookie");
System.out.println(h2);
Header[] hs = response.getHeaders("Set-Cookie");
System.out.println(hs.length);

输出:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
2

获取所有的头部信息的最有效的方法是使用HeaderIterator接口:

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");

HeaderIterator it = response.headerIterator("Set-Cookie");

while (it.hasNext()) {
    System.out.println(it.next());
}

输出:

Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"

HttpClient同时还提供了简易的方法,来将HTTP信息解析为单个的头部元素(HeaderElement):

HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK");
response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost");
response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\"");

HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator("Set-Cookie"));

while (it.hasNext()) {
    HeaderElement elem = it.nextElement(); 
    System.out.println(elem.getName() + " = " + elem.getValue());
    NameValuePair[] params = elem.getParameters();
    for (int i = 0; i < params.length; i++) {
        System.out.println(" " + params[i]);
    }
}

输出:

c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost

HTTP实体

HTTP消息可能包含着一个和请求或者响应相关的实体,因为它是可选的。包含了实体的请求被称为实体封闭请求。HTTP规范定义了两种实体封闭请求方法:postput。响应通常都包含一个内容实体。当然,也有例外,例如HEAD方法的响应以及204 No Content304 Not Modified205 Reset Content的响应。

HttpClient根据其内容的来源,将实体分为三类:

  • 流式(streamed)实体:内容从流中获得,或者在运行时产生。尤其是,这种流包含了从HTTP响应中接收到的实体。流式实体一般是不可重复的。
  • 自我包含式(self-contained)实体:内容存在于内存中,或者通过其它独立于连接或其它实体的方式获得。自我包含式实体一般来说是可重复的。这类实体通常用在实体封闭的HTTP请求中。
  • 包裹式(wrapping)实体:内容从另外一个实体中取得。

当从HTTP响应中获取流式内容时,这种区分对于连接管理是至关重要的。对于应用程序创建的、只使用HttpClient发送的请求实体来说,流式和自我包含式实体的区别就没那么重要了。那样的话,建议将不可重复的实体看作流式实体,而可重复的实体则看作是自我包含式实体。

可重复实体

一个实体是可重复的,意味着它的信息可以被多次读取。只有当实体是自我包含式的时候才是可能的(如ByteArrayEntityStringEntity)。

使用HTTP实体

既然一个实体既可以表示二进制信息,也可表示字符串信息,那么它就支持字符的编码。

实体是在执行封闭信息的请求时,或者请求成功,响应体发送结果到客户端时产生的。

要读取实体中的信息,可以使用HttpEntity#getContent()方法获取一个输入流,这样会返回一个java.io.InputStream,或者给HttpEntity#writeTo(OutputStream)提供一个输出流,这种情况会一次返回所有被写入到给定流中的内容。

当接收到带有消息的实体时,可以使用HttpEntity#getContentType()HttpEntity#getContentLength()方法读取通用的元数据,如Content-TypeContent-Length头。由于Content-Type头可能包含了对文本MIME类型的编码,如text/plain或者text/html,HttpEntity#getContentEncoding()就用于读取这段信息。如果头部信息不可用,就会返回长度为-1,content type为NULL。如果Content-Type是可用的,就会返回一个Header对象。

在为发送的消息创建实体时,元数据必须由实体创建者提供。

StringEntity myEntity = new StringEntity("important message", ContentType.create("text/plain", "UTF-8"));

System.out.println(myEntity.getContentType());
System.out.println(myEntity.getContentLength());
System.out.println(EntityUtils.toString(myEntity));
System.out.println(EntityUtils.toByteArray(myEntity).length);

输出:

Content-Type: text/plain; charset=utf-8
17
important message
17

确保低级资源的释放

为了保证系统资源的释放,必须关闭和实体相关的信息流和实体本身。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        InputStream instream = entity.getContent();
        try {
            // do something useful
        } finally {
            instream.close();
        }
    }
} finally {
    response.close();
}

关闭信息流和关闭响应的区别在于,前者会试图通过消耗实体的方式保持连接,而后者会立刻关闭并抛弃连接。

请注意,一旦被万端输出,HttpEntity#writeTo(OutputStream)方法也需要保证系统资源的释放。如果这个方法通过调用HttpEntity#getContent()方法包含有一个java.io.InputStream实例,那么这个流也需要在finally中被关闭。

当使用流式实体时,可以使用EntityUtils#consume(HttpEntity)方法来保证实体信息都被完全消耗,并且底层的流也都被关闭。

然而有可能出现这样的情况:整个响应信息只有一小部分需要被取出,而消耗剩余的信息、使连接不可用的成本太高了,在这种情况下,就可以直接通过关闭响应终止信息流。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        InputStream instream = entity.getContent();
        int byteOne = instream.read();
        int byteTwo = instream.read();
        // Do not need the rest
    }
} finally {
    response.close();
}

这个连接不可被重新使用,但是它占用所有级别的资源都被正确地释放。

消耗实体信息

推荐的消耗实体信息的方式是调用HttpEntity#getContent()方法或者HttpEntity#writeTo(OutputStream)方法。HttpClient还提供了EntityUtils类,该类提供了许多静态方法,可以更加容易地从实体中读取内容或信息。通过使用该类的方法,就可以获取字符串/字节数组形式的内容体,而不是直接使用java.io.InputStream。然而,使用EntityUtils是强烈不推荐的,除非响应实体来自一个可信任的HTTP服务器,而且内容长度限制是已知的。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
    HttpEntity entity = response.getEntity();
    if (entity != null) {
        long len = entity.getContentLength();
        if (len != -1 && len < 2048) {
            System.out.println(EntityUtils.toString(entity));
        } else {
            // Stream content out
        }
    }
} finally {
    response.close();
}

在某些情况下可能需要多次读取实体信息。这时,实体信息就需要被缓冲在内存上或磁盘上。最简单的方法就是用BufferedHttpEntity类包裹实体,这样的话,原实体会被读取进内存缓冲区中。在其他所有的方式中,实体包装器都会得到原实体。

CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
    entity = new BufferedHttpEntity(entity);
}

产生实体信息

HttpClient提供了许多类,用于高效地将内容从HTTP连接中读取出来。这些类的实体可以和实体封闭请求(如POSTPUT)联系起来,旨在封闭实体,从HTTP请求中获取输出内容。HttpClient为许多通用的数据容器都提供了相应的类,如字符串、字节数组、输入流和文件:StringEntityByteArrayEntityInputStreamEntityFileEntity

File file = new File("somefile.txt");
FileEntity entity = new FileEntity(file, ContentType.create("text/plain", "UTF-8"));        

HttpPost httppost = new HttpPost("http://localhost/action.do");
httppost.setEntity(entity);

请注意,InputStreamEntity是不可重复的,因为它只可以从底层数据流中读取一次。通常来说,我们推荐实现一个自定义的HttpEntity类,它是自我包含的,来代替通用的InputStreamEntityFileEntity就是一个很好的开端。

HTML表单

许多应用程序需要模拟提交一个HTML表单的过程,例如,为了登陆一个网络应用程序或者提交输入信息。HttpClient提供了实体类UrlEncodedFormEntity来简化这一过程。

List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair("param1", "value1"));
formparams.add(new BasicNameValuePair("param2", "value2"));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, Consts.UTF_8);
HttpPost httppost = new HttpPost("http://localhost/handler.do");
httppost.setEntity(entity);

UrlEncodedFormEntity实例会使用所谓的URL编码来为参数编码,产生以下的内容:

param1=value1&param2=value2

内容分块

通常,我们推荐让HttpClient来基于正在传输的HTTP信息选择最合适的传输编码。然而,也可以通过将HttpEntity#setChunked()为true来告知HttpClient优先使用分块编码。请注意,HttpClient只会讲该标识作为一个提示。这个值会在使用不支持分块编码的HTTP协议版本时被忽略,如HTTP/1.0。

StringEntity entity = new StringEntity("important message", ContentType.create("plain/text", Consts.UTF_8));
entity.setChunked(true);
HttpPost httppost = new HttpPost("http://localhost/acrtion.do");
httppost.setEntity(entity);

响应处理器

最简单快捷的处理响应的方法是使用ResponseHandler接口,该接口包含了handleResponse(HttpResponse response)方法。该方法帮助用户解决了连接管理的问题。当使用ResponseHandler时,HttpClient会自动保证连接被释放回到连接管理器中,无论请求处理成功,还是导致了一个异常。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/json");

ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() {

    @Override
    public JsonObject handleResponse(
            final HttpResponse response) throws IOException {
        StatusLine statusLine = response.getStatusLine();
        HttpEntity entity = response.getEntity();
        if (statusLine.getStatusCode() >= 300) {
            throw new HttpResponseException(
                    statusLine.getStatusCode(),
                    statusLine.getReasonPhrase());
        }
        if (entity == null) {
            throw new ClientProtocolException("Response contains no content");
        }
        Gson gson = new GsonBuilder().create();
        ContentType contentType = ContentType.getOrDefault(entity);
        Charset charset = contentType.getCharset();
        Reader reader = new InputStreamReader(entity.getContent(), charset);
        return gson.fromJson(reader, MyJsonObject.class);
    }
};
MyJsonObject myjson = client.execute(httpget, rh);

HttpClient接口

HttpClient接口代表着HTTP请求处理中最重要的协议。它没有对请求执行过程施加任何限制或者特殊细节,而是将连接管理、状态管理、身份验证和重定向处理留给单个实现类来处理。这将简化使用诸如响应内容缓存之类的额外功能来装饰接口。

通常,HttpClient实现类充当许多特殊用途处理程序或者策略接口实现类的外观(facade),这些程序或者实现类负责处理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();

HttpClient线程安全性

HttpClient的实现类应当是线程安全的。我们推荐该类的一个实例可以在多请求处理中复用。

HttpClient资源回收

当一个CloseableHttpClient实例不再需要,而且即将超出作用域时,必须通过调用CloseableHttpClient#close()方法关闭与之关联的连接管理器。

CloseableHttpClient httpclient = HttpClients.createDefault();
try {
    <...>
} finally {
    httpclient.close();
}

Tips

注意,所有HttpComponents提供的HttpClient的实现类(CloseableHttpClientContentEncodingHttpClientDecompressingHttpClientDefaultHttpClientSystemDefaultHttpClientAbstractHttpClient),在HtpClient4.3以后,除了CloseableHttpClient外都已废弃,而CloseableHttpClient的构造方法也已废弃,应使用HttpClientBuilder类来创建CloseableHttpClient

HTTP执行上下文

最初,HTTP被设计为一种无状态的、面向请求-响应的协议。然而,事实上应用程序通常需要通过几个逻辑上的请求-响应交换来持久化状态信息。为了使应用程序能够维持一个持续处理的状态,HttpClient允许在一个特定的处理上下文中执行请求,这被称为HTTP上下文。如果在连续的请求间重用相同的上下文,那么多个逻辑相关的请求就可以参与同一个逻辑会话(Session)。HTTP上下文和java.util.Map<String, Object>起到的功能很相似。它仅仅是一组键值对。应用程序可以在请求执行之前填充上下文属性,或者在执行完成后检查上下文。

HttpContext可以包含任意的对象,因此,在多线程间共享可能是不安全的。我们推荐每个处理线程维护自己的上下文。

在HTTP请求执行的过程中,HttpClient向执行上下文添加以下的属性:

  • HttpConnection,代表实际的连接实例。
  • HttpHost,代表连接目标实例。
  • HttpRoute,代表连接路由实例。
  • HttpRequest,代表HTTP请求实例。执行上下文中最终的HTTP请求对象总是代表消息发送到目标服务器时的状态。对于默认的HTTP/1.0和HTTP/1.0协议,使用相对的请求URI。然而如果请求时通过一个无隧道模式的代理发出的,那么URI就是绝对地址。
  • HttpResponse,代表HTTP响应实例。
  • java.lang.Boolean,代表请求是否已经被完全传送到连接目标的标识对象。
  • RequestConfig,代表请求配置的对象。
  • java.util.List<URI>代表在请求处理过程中收到的所有重定位地址的对象。

用户可以使用HttpClientContext适配器来简化对上下文状态的交互。

HttpContext context = <...>
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpHost target = clientContext.getTargetHost();
HttpRequest request = clientContext.getRequest();
HttpResponse response = clientContext.getResponse();
RequestConfig config = clientContext.getRequestConfig();

代表一次逻辑相关的会话的请求序列应当在同一个HttpContext实例中被执行,来确保会话上下文和请求之间的状态信息的自动传递。

在下面的例子中,初始请求设置的请求配置将保存在执行上下文中,并传递到共享相同上下文的连续请求中。

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();
}

HTTP协议拦截器

HTTP协议拦截器是一个实现了HTTP协议中一个特定方面的例程。通常,协议拦截器应当作用于传入的消息的一个特定的头部或者一组相关的头部,或者用一个特定的头部或者一组相关的头部来标识传出的信息。协议拦截器也可以操作包含信息的内容实体——透明内容的压缩/解压就是一个很好的例子。通常这是使用“装饰器”模式实现的,其中包装实体类被用于装饰原始实体。多个协议拦截器可以被组合在一起,形成一个逻辑单元。

协议拦截器可以通过在HTTP执行上下文中共享信息来进行协作——例如处理状态。协议拦截器可以使用HTTP上下文来为一个请求或多个连续请求保存处理状态。

通常,只要拦截器不依赖于执行上下文中的一个特定的状态,拦截器的执行顺序就是无关紧要的。如果拦截器之间存在相互依赖关系,必须以一个特定的顺序执行,那么他们就应当按照执行的顺序被添加进协议处理器。

协议拦截器的实现应当是线程安全的。和servlet相似,协议拦截器不应当使用实例化的变量,除非对这些变量的访问是同步的。

以下是如何使用本地上下文在连续请求之间持久化处理状态的例子:

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();
    }
}

异常处理

HTTP协议拦截器可能抛出两种类型的异常:当发生例如套接字超时或者套接字重置之类的I/O异常时的java.io.Exception,以及当发生例如违反HTTP协议之类的HTTP异常时的HttpException。通常,I/O错误被认为是不重要且可恢复的,而HTTP协议错误则相反,是严重而且无法自动恢复的。请注意,Httplient的实现类将HttpException作为ClientProtocolException异常抛出,这是java.io.IOException的子类。这使得HttpClient的用户可以在一次捕捉中同时处理I/O错误和协议错误。

HTTP传输安全

HTTP协议并不总是完美适用于所有类型的应用,理解这一点是很重要的。HTTP是一个简单的面向请求/响应的协议,最初被设计来支持静态或者动态产生的内容获取。它从未被想过支持事务型(transactional)操作。例如,如果HTTP服务器成功地接收和处理请求、生成响应并将状态代码发送回客户机,那么它将认为它完成了协议的一部分。如果客户端由于读取超时、请求取消或者系统崩溃之类的原因没能收到响应,服务器也不会试图去回滚(roll back)事务。如果客户端决定重新发送相同的请求,那么服务器就不可避免地多次执行相同的事务。在某些情况下,这可能导致应用程序数据损坏或应用程序状态不一致。

即使HTTP从未被设计支持事务处理,但只要满足某些条件,它仍然能作为关键任务应用程序传输的协议。为了保证HTTP传输层安全,系统必须保证HTTP方法在应用层的幂等性。

幂等性方法

HTTP/1.1规范中这样定义一个幂等性方法:

【如果N>0次相同请求的效果和单次请求的效果相同,那么就称方法具有幂等属性(除了发生错误或者可预料的事件)】

换句话说,应用程序应当保证准备好处理对同一方法的多次执行的影响和后果。例如,可以通过提供唯一的事务id和其他避免执行相同逻辑操作的方法来实现这一点。

请注意,这个问题不是HttpClient所特有的。基于浏览器的应用程序都会受到与HTTP方法非幂等性完全相同的问题的影响。

默认情况下,HttpClient仅假设非实体封闭的方法(如GETHEAD)是幂等的,而处于兼容性的原因,实体封装方法(如POSTPUT)则不是幂等的。

异常自动恢复

默认情况下,HttpClient会试图从I/O异常中自动恢复。默认的自动恢复机制仅限于几个已知安全的异常。

  • HttpClient不会试图从任何逻辑错误或者HTTP协议错误(从HttpException中派生出的错误)中恢复。
  • HttpClient将自动重试那些被假定为幂等的方法。
  • 当HTTP请求仍在传输到目标服务器(即请求尚未完全传输到服务器)时,HttpClient将自动重试那些在传输异常情况下发生错误的方法。

重试请求处理器

为了启用自定义的异常恢复机制,用户应该提供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中被定义为安全可自动重试的方法:GETHEADPUTDELETEOPTIONSTRACE

异常终止的请求

某些情况下,由于目标服务器的高负载或者客户端的高并发请求,HTTP请求没能在预期的时间内执行完成。在这种情况下就可能有必要提前终止请求,并解除由于I/O操作而被阻塞的处理线程的阻塞状态。正在被HttpClient执行的请求可以在执行的任何阶段通过调用HttpUriRequest#abort()方法强行终止。该方法是线程安全的,而且可以在任何线程中调用。当一个HTTP请求被终止时,它的执行线程——即使暂时被I/O操作阻塞——也会通过抛出InterruptedIOException异常来保证解除阻塞。

重定向处理

HttpClient自动处理所有类型的重定向,除了HTTP规范明确禁止的需要用户干预的重定向。POSTGET请求时的303状态码就会被转换为HTTP规范中所要求的GET请求。用户可以使用自定义的重定向策略来减少HTTP规范对POST方法自动重定向的限制。

LaxRedirectStrategy redirectStrategy = new LaxRedirectStrategy();
CloseableHttpClient httpclient = HttpClients.custom()
        .setRedirectStrategy(redirectStrategy)
        .build();

HttpClient在执行的过程中有时会覆写请求消息。默认的HTTP/1.0和HTTP/1.1通常使用相对请求URI。类似的,最初的请求也有可能多次从一个位置重定向到另一个位置。最终解析的绝对的HTTP位置可以使用最初的请求和上下文构建。工具方法URIUtils#resolve可用于构建用于生成最终请求的解释的绝对URI。此方法包括来自重定向请求或原始请求的最后一个片段标识符。

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();
}

写在最后的话

本来想只是写一点HttpComponents的入门小教程的,结果看完官方的教程才发现,差的太多,太多概念不理解。为了不误人子弟,只得一点一点翻译官方教程。该教程来自HttpClient Tutorial的第一章节:Fundamentals,地址http://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/fundamentals.html 。

猜你喜欢

转载自blog.csdn.net/qq_40856284/article/details/106499419