HttpClient-4.5.2官方教程完整翻译

HttpClient 4.5.2

前言

超文本传输协议(HTTP)可能是当今互联网上使用的最重要的协议。
网络服务,支持网络的设备以及网络计算的发展继续扩大了HTTP协议在用户驱动的Web浏览器之外的作用,同时增加了需要HTTP支持的应用程序的数量。

尽管java.net包提供了通过HTTP访问资源的基本功能,但它并不能提供许多应用程序所需的全部灵活性或功能。
HttpClient试图通过提供一个高效的,最新的,功能丰富的包来实现最新的HTTP标准和建议的客户端来填补这个空白。

为扩展而设计,同时为基本的HTTP协议提供强大的支持,任何构建HTTP感知的客户端应用程序(例如Web浏览器,Web服务客户端或利用或扩展HTTP协议进行分布式通
信的系统)的HttpClient都可能是有用的。

HttpClient scope

  • 基于HttpCore的客户端HTTP传输库
  • 基于经典(阻塞)I / O
  • 内容不可知的

HttpClient 不是什么

HttpClient不是一个浏览器。这是一个客户端HTTP传输库。HttpClient的目的是发送和接收HTTP消息。HttpClient不会尝试处理内容,执行嵌入HTML页面的
JavaScript,尝试猜测内容类型(如果没有明确设置),或者重新格式化请求/重写位置URI或与HTTP传输无关的其他功能。

基础

请求的执行

HttpClient最重要的功能是执行HTTP方法。执行HTTP方法涉及一个或多个HTTP请求/ HTTP响应交换,通常由HttpClient内部处理。
用户需要提供一个用于执行的请求对象,HttpClient需要向目标服务器发送请求并得到相应的响应对象,如果执行不成功则抛出异常。
自然地,定义以上内容的接口就是HttpClient API的主要入口点。
下面是最简单的请求执行过程示例:

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规范中定义的所有HTTP方法:GET,HEAD,POST,PUT,DELETE,TRACE和OPTIONS。每个方法类型都有一个特定的类:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。

Request-URI是一个统一资源标识符,用于标识应用请求的资源。HTTP请求URI由协议方案,主机名,可选的端口,资源路径,可选的查询和可选的片段组成。

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

HttpClient提供URIBuilder实用程序类来简化请求URI的创建和修改。

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响应是服务器收到请求消息后执行相关操作后发送回客户端的消息。该消息的第一行由协议版本和数字状态码及其相关的文本短语组成。

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

使用 Message Header

HTTP消息可以包含许多描述消息属性的header,如内容长度,内容类型等等。HttpClient提供了检索,添加,删除和枚举header的方法。

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

获取给定类型的headers的最有效的方法是使用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消息解析为单独的header元素。

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 Entity

HTTP消息可以携带与请求或响应相关联的内容实体。实体只可以在一些请求和一些响应中找到,因为它们是可选的。使用实体的请求被称为实体封闭请求(entity enclosing requests)。HTTP规范定义了两个实体封闭请求方法:POST和PUT。响应通常会携带消息体。此规则也有例外情况,例如对HEAD方法的响应和对204无内容、304未修改、205重置内容的响应。

  • streamed: 内容是从一个流接收的,或者是随时产生的。具体来说,这个类别包括从HTTP响应中收到的实体。流派实体通常不可重复。
  • self-contained: 内容在内存中或通过独立于连接或其他实体的方式获得。该类实体通常可重复。这种类型的实体将主要用于包含HTTP请求的实体。
  • wrapping: 内容是从另一个Entity获得的。

当使用流从响应中读取内容时,这种区别对于连接管理非常重要。对于由应用程序创建并仅使用HttpClient发送的请求实体,流式和自包含之间的区别并不重要。在这种情况下,建议将不可重复的实体视为流式,将可重复的实体视为独立式。

可重复的Entity

一个实体可以是可重复的,这意味着它的内容可以被多次读取。这只适用于自包含的实体(如ByteArrayEntity或StringEntity)

使用 Http Entities

由于实体可以包含二进制和字符内容,因此它支持字符编码(以支持后者,即字符内容)。

在执行带有封闭内容的请求时或者请求成功后使用响应主体将结果发送回客户端时创建实体。

要读取entity内容可以使用HttpEntity的getContent() 方法,该方法返回一个 java.io.InputStream,或者提供一个输出流给HttpEntity的writeTo(OutputStream)方法,该方法会将entity的内容写入到给定的输出流。

当收到一个传入消息的实体时,HttpEntity的getContentType()方法和getContentLength()方法可以用来读取元数据,例如Content-Type 和 Content-Length headers(这些数据如果已经被提供)。由于Content-Type头可以包含文本MIME类型(如text / plain或text / html)的字符编码,因此使用HttpEntity#getContentEncoding()方法来读取此编码。如果header不可用,则将返回长度为-1,内容则返回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类,它公开了几个静态方法来更容易地从一个实体读取内容或信息。

通过这些方法可以使用String或byte取回所有消息体内容而不是直接使用io流。
但是,强烈建议不要使用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连接有效地流式输出内容。这些类的实例可以与实体封闭请求(如POST和PUT)相关联,可以将实体内容包含在传出的HTTP请求中。HttpClient为大多数常用数据容器(如字符串,字节数组,输入流和文件)提供了几个类:StringEntity,ByteArrayEntity,InputStreamEntity和FileEntity。

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类,它是自包含的,而不是使用通用的InputStreamEntity。 FileEntity可以是一个很好的起点。

HTML 表单

例如,许多应用程序需要模拟提交HTML表单的过程,以便登录到Web应用程序或提交输入数据。

HttpClient提供了实体类UrlEncodedFormEntity来便捷实现过程。

List formparams = new ArrayList();
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¶m2=value2

内容分块(content chunking)

通常建议让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响应)方法。
这个方法完全缓解了用户对于连接管理的担心。
当使用ResponseHandler时,无论请求执行成功还是导致异常,HttpClient都会自动确保连接释放回连接管理器。

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

ResponseHandler rh = new ResponseHandler() {

    @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实现是作为一个特殊用途处理程序的外观或负责处理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();
}

HTTP 执行上下文

最初,HTTP被设计为一种无状态,面向响应请求的协议。但是,真实世界的应用程序通常需要能够通过几个逻辑相关的请求 - 响应交换来保存状态信息。为了使应用程序保持处理状态,HttpClient允许HTTP请求在特定的执行上下文(被称为HTTP上下文)内执行。如果在连续的请求之间重复使用相同的上下文,多个逻辑相关的请求可以参与逻辑会话。HTTP上下文的功能类似于java.util.Map

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.IOException异常,以及发出HTTP异常(如违反HTTP协议)的HttpException。通常I / O错误被认为是非致命的,可恢复的,而HTTP协议错误被认为是致命的,不能自动从中恢复。请注意,HttpClient实现将HttpExceptions重新抛出为ClientProtocolException,它是java.io.IOException的子类。这使得HttpClient的用户可以从单个catch子句处理I / O错误和协议违规。

HTTP传输安全

HTTP协议并不适用于所有类型的应用程序,这一点很重要。HTTP是一种简单的面向请求/响应的协议,最初设计用来支持静态或动态生成的内容检索。从来没有打算支持交易操作。例如,如果HTTP服务器成功地接收并处理请求,产生响应并将状态码发送回客户端,则HTTP服务器将认为其合同的一部分被执行。如果客户端由于读取超时,请求取消或系统崩溃而未能全部收到响应,服务器将不会尝试回滚事务。如果客户决定重试相同的请求,服务器将不可避免地不止一次地执行相同的事务。在某些情况下,这可能会导致应用程序数据损坏或应用程序状态不一致。

尽管HTTP从未被设计为支持事务处理,但是在满足特定条件的情况下,它仍然可以用作关键应用程序的传输协议。为了确保HTTP传输层的安全,系统必须确保应用层HTTP方法的幂等性。

幂等方法

HTTP / 1.1规范定义了一个幂等方法 [方法也可以具有“幂等性”的性质(除了错误或期满问题)N> 0相同请求的副作用与单个请求相同]
换句话说,应用程序应该确保它准备好处理同一方法的多次执行的影响。这可以通过例如提供唯一的事务ID以及通过避免执行相同的逻辑操作的其他方式来实现。

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

默认情况下,HttpClient假定只有非实体封闭的方法(如GET和HEAD)是幂等的,实体封装方法(如POST和PUT)不是出于兼容性的原因。

自动异常恢复

默认情况下,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定义为幂等的请求方法视为安全自动重试:GET,HEAD,PUT,DELETE,OPTIONS和TRACE。

中止请求

在某些情况下,由于目标服务器的高负载或客户端发出的太多并发请求,HTTP请求执行无法在预期的时间范围内完成。在这种情况下,可能需要提前终止请求,并解除在I / O操作中阻塞的执行线程。由HttpClient执行的HTTP请求可以通过调用HttpUriRequest#abort()方法在任何执行阶段中止。这个方法是线程安全的,可以从任何线程调用。当一个HTTP请求被中止时,它的执行线程 - 即使当前被阻塞在一个I / O操作中 - 通过抛出一个InterruptedIOException来保证解除阻塞。

处理重定向

HttpClient自动处理所有类型的重定向,除了HTTP规范明确禁止的重定向,需要用户干预。请参阅其他(状态码303)重定向POST和PUT请求转换为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 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();
}

连接管理

连接持久性

建立从一个主机到另一个主机的连接的过程相当复杂,并且涉及两个端点之间的多个分组交换,这可能相当耗时。连接握手的开销可能很大,特别是对于小型的HTTP消息。如果可以重新使用开放连接来执行多个请求,则可以实现更高的数据吞吐量。

HTTP / 1.1规定HTTP连接可以重复用于多个请求。符合HTTP / 1.0的端点还可以使用一种机制来显式传达它们的首选项,以保持连接的活动状态并将其用于多个请求。HTTP代理还可以保持空闲连接在一段时间内保持活动状态,以便后续请求连接到同一个目标主机。保持连接的能力通常被称为连接持久性。HttpClient完全支持连接持久性。

HTTP连接路由

HttpClient能够直接或通过可能涉及多个中间连接(也称为中继)的路由建立到目标主机的连接。

HttpClient将路由的连接区分为普通,隧道和分层。使用多个中间代理来隧道连接到目标主机被称为代理链接。

plain路由是直接连接到目标或只经过一个代理连接到目标服务器建立的。隧道路由(Tunnelled routes)是通过连接到第一个隧道,并通过代理链向目标隧道建立的。没有代理的路由不能被隧道化。分层路由(Layered routes)通过在现有连接上分层协议来建立。协议只能在通往目标的隧道上进行分层,或者通过无代理的直接连接进行分层。

路由计算

RouteInfo接口包含到达目标主机的一个路由的信息,该路由涉及一个或多个中间步骤或跳跃。HttpRoute是RouteInfo的具体实现,它不能被改变(是不可变的)。HttpTracker是HttpClient内部使用的一个可变的RouteInfo实现,用于跟踪剩余的跳转到最终的路由目标。HttpTracker可以在向路由目标成功执行下一跳之后更新。HttpRouteDirector是一个辅助类,可以用来计算路由中的下一步。这个类由HttpClient在内部使用。

HttpRoutePlanner是一个接口,它代表一个基于执行上下文来计算给定目标的完整路由的策略。HttpClient附带两个默认的HttpRoutePlanner实现。SystemDefaultRoutePlanner基于java.net.ProxySelector。默认情况下,它将从系统属性或运行应用程序的浏览器中获取JVM的代理设置。DefaultProxyRoutePlanner实现不使用任何Java系统属性,也不使用任何系统或浏览器代理设置。它总是通过相同的默认代理来计算路由。

安全的HTTP连接

如果在两个连接端点之间传输的信息不能被未经授权的第三方读取或篡改,那么HTTP连接可以被认为是安全的。SSL / TLS协议是确保HTTP传输安全性的最广泛使用的技术。但是,也可以使用其他加密技术。通常,HTTP传输被SSL / TLS加密连接分层。

HTTP连接管理器

连接的管理和连接管理器

HTTP连接是复杂的,有状态的,线程不安全的对象,需要妥善管理才能正常工作。HTTP连接一次只能由一个执行线程使用。HttpClient使用一个特殊的实体来管理HTTP连接的访问,称为HTTP连接管理器,并由HttpClientConnectionManager接口表示。HTTP连接管理器的目的是作为新的HTTP连接的工厂,管理持久连接的生命周期,并同步对持久连接的访问,以确保一次只有一个线程可以访问连接。内部HTTP连接管理器与ManagedHttpClientConnection实例一起工作,作为管理连接状态和控制I / O操作执行的真实连接的代理。如果托管连接被释放或被其消费者明确关闭,则底层连接从其代理分离,并返回给管理器。即使服务消费者仍然持有对代理实例的引用,它不再有意或无意地执行任何I / O操作或改变真实连接的状态。

这是从连接管理器获取连接的示例:

HttpClientContext context = HttpClientContext.create();
HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
HttpRoute route = new HttpRoute(new HttpHost("localhost", 80));
// Request new connection. This can be a long process
ConnectionRequest connRequest = connMrg.requestConnection(route, null);
// Wait for connection up to 10 sec
HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
try {
    // If not open
    if (!conn.isOpen()) {
        // establish connection based on its route info
        connMrg.connect(conn, route, 1000, context);
        // and mark it as route complete
        connMrg.routeComplete(conn, route, context);
    }
    // Do useful things with the connection.
} finally {
    connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
}

如有必要,可以通过调用ConnectionRequest#cancel()来过早终止连接请求。这将解除在ConnectionRequest#get()方法中阻塞的线程。

简单连接管理

BasicHttpClientConnectionManager是一个简单的连接管理器,一次只维护一个连接。即使这个类是线程安全的,它也只能被一个执行线程使用。BasicHttpClientConnectionManager将努力重复使用相同路由的后续请求的连接。但是,如果持续连接的路由与连接请求的路由不匹配,它将关闭现有连接并重新打开给定路由。如果连接已被分配,则引发java.lang.IllegalStateException。

这个连接管理器的实现应该在EJB容器中使用。

连接池连接管理

PoolingHttpClientConnectionManager是一个更复杂的实现,它管理一个客户端连接池,并能够处理来自多个执行线程的连接请求。连接按照其路由进行汇集。对于管理器已经在池中具有持续连接的路由的请求将通过从池租用连接而不是创建全新的连接来进行服务。

PoolingHttpClientConnectionManager保持针对一个路由的连接和所有连接的最大限制。默认情况下,每个给定的路由创建不超过2个并发连接,总共不超过20个连接。对于许多真实世界的应用程序来说,这些限制可能被证明过于严格,特别是如果他们使用HTTP作为其服务的传输协议。

此示例显示连接池参数如何调整:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);

CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

关闭连接管理器

当一个HttpClient实例不再需要并且即将离开作用域时,关闭它的连接管理器以确保管理器内活着的所有连接关闭并释放由这些连接分配的系统资源是非常重要的。

CloseableHttpClient httpClient = <...>
httpClient.close();

多线程执行请求

当配备PoolingClientConnectionManager等连接池管理器时,可以使用HttpClient同时使用多个执行线程执行多个请求。

PoolingClientConnectionManager将根据其配置分配连接。如果给定路由的所有连接已经租用,连接请求将被阻塞,直到连接释放回池。可以通过将“http.conn-manager.timeout”设置为正值来确保连接管理器不会在连接请求操作中无限期地阻塞。如果连接请求在给定的时间内无法被服务,则抛出ConnectionPoolTimeoutException。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

// URIs to perform GETs on
String[] urisToGet = {
    "http://www.domain1.com/",
    "http://www.domain2.com/",
    "http://www.domain3.com/",
    "http://www.domain4.com/"
};

// create a thread for each URI
GetThread[] threads = new GetThread[urisToGet.length];
for (int i = 0; i < threads.length; i++) {
    HttpGet httpget = new HttpGet(urisToGet[i]);
    threads[i] = new GetThread(httpClient, httpget);
}

// start the threads
for (int j = 0; j < threads.length; j++) {
    threads[j].start();
}

// join the threads
for (int j = 0; j < threads.length; j++) {
    threads[j].join();
}

虽然HttpClient实例是线程安全的并且可以在多个执行线程之间共享,但强烈建议每个线程维护自己的HttpContext专用实例。

static class GetThread extends Thread {

    private final CloseableHttpClient httpClient;
    private final HttpContext context;
    private final HttpGet httpget;

    public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
        this.httpClient = httpClient;
        this.context = HttpClientContext.create();
        this.httpget = httpget;
    }

    @Override
    public void run() {
        try {
            CloseableHttpResponse response = httpClient.execute(
                    httpget, context);
            try {
                HttpEntity entity = response.getEntity();
            } finally {
                response.close();
            }
        } catch (ClientProtocolException ex) {
            // Handle protocol errors
        } catch (IOException ex) {
            // Handle I/O errors
        }
    }

}

连接驱逐策略

经典阻塞I / O模型的主要缺点之一是网络套接字只有在I / O操作中被阻塞时才能对I / O事件做出反应。当一个连接释放回管理器时,它可以保持活动状态,但是它无法监视套接字的状态并对任何I / O事件做出反应。如果连接在服务器端被关闭,客户端连接将无法检测到连接状态的变化(并通过关闭套接字来适当地作出反应)。

HttpClient试图在使用连接执行HTTP请求之前测试连接是否是“陈旧的”,来缓解这个问题,陈旧的连接不再有效,因为它是在服务器端关闭。陈旧的连接检查不是100%可靠的。唯一可行解决方案是,对于空闲连接,使用一个专用的监视器线程,该线程是不涉及每个套接字模型的一个线程,用于驱除由于长时间不活动而被认为已过期的连接。监视线程可以定期调用ClientConnectionManager#closeExpiredConnections()方法关闭所有过期的连接,并从池中驱逐关闭的连接。它还可以选择调用ClientConnectionManager#closeIdleConnections()方法来关闭在给定时间段内闲置的所有连接。

public static class IdleConnectionMonitorThread extends Thread {

    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;

    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }

    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }

}

连接保活策略

HTTP规范没有指定持续连接可能会保持多久,应该保持活动状态。一些HTTP服务器使用一个非标准的Keep-Alive标头来向客户端传达他们希望在服务器端保持连接的时间段(以秒为单位)。如果可用的话,HttpClient使用这个信息。如果响应中不存在Keep-Alive头,则HttpClient假定连接可以无限期地保持活动状态。但是,通常使用的许多HTTP服务器被配置为在一段时间不活动之后丢弃持久连接,以节省系统资源,而通常不通知客户端。如果默认策略过于乐观,则可能需要提供自定义保活策略。

ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {

    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
        // Honor 'keep-alive' header
        HeaderElementIterator it = new BasicHeaderElementIterator(
                response.headerIterator(HTTP.CONN_KEEP_ALIVE));
        while (it.hasNext()) {
            HeaderElement he = it.nextElement();
            String param = he.getName();
            String value = he.getValue();
            if (value != null && param.equalsIgnoreCase("timeout")) {
                try {
                    return Long.parseLong(value) * 1000;
                } catch(NumberFormatException ignore) {
                }
            }
        }
        HttpHost target = (HttpHost) context.getAttribute(
                HttpClientContext.HTTP_TARGET_HOST);
        if ("www.naughty-server.com".equalsIgnoreCase(target.getHostName())) {
            // Keep alive for 5 seconds only
            return 5 * 1000;
        } else {
            // otherwise keep alive for 30 seconds
            return 30 * 1000;
        }
    }

};
CloseableHttpClient client = HttpClients.custom()
        .setKeepAliveStrategy(myStrategy)
        .build();

连接套接字工厂

HTTP连接在内部使用java.net.Socket对象来处理通过线路传输的数据。但是他们依靠ConnectionSocketFactory接口来创建,初始化和连接套接字。这使得HttpClient的用户可以在运行时提供特定于应用程序的套接字初始化代码。PlainConnectionSocketFactory是创建和初始化普通(未加密)套接字的默认工厂。创建套接字并将其连接到主机的过程是分离的,以便在连接操作中阻塞套接字。

HttpClientContext clientContext = HttpClientContext.create();
PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();
Socket socket = sf.createSocket(clientContext);
int timeout = 1000; //ms
HttpHost target = new HttpHost("localhost");
InetSocketAddress remoteAddress = new InetSocketAddress(
        InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);
sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);

安全的套接字分层

LayeredConnectionSocketFactory是ConnectionSocketFactory接口的扩展。分层的套接字工厂能够在现有的普通套接字上创建套接字。套接字分层主要用于通过代理创建安全套接字。HttpClient附带实现SSL / TLS分层的SSLSocketFactory。请注意HttpClient不使用任何自定义加密功能。它完全依赖于标准的Java加密(JCE)和安全套接字(JSEE)扩展。

与连接管理器集成

自定义连接套接字工厂可以与特定协议方案(如HTTP或HTTPS)相关联,然后用于创建自定义连接管理器。

ConnectionSocketFactory plainsf = <...>
LayeredConnectionSocketFactory sslsf = <...>
Registry r = RegistryBuilder.create()
        .register("http", plainsf)
        .register("https", sslsf)
        .build();

HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
HttpClients.custom()
        .setConnectionManager(cm)
        .build();

SSL/TLS定制

HttpClient使用SSLConnectionSocketFactory创建SSL连接。 SSLConnectionSocketFactory允许高度的自定义。它可以将javax.net.ssl.SSLContext的实例作为参数,并使用它创建自定义配置的SSL连接。

KeyStore myTrustStore = <...>
SSLContext sslContext = SSLContexts.custom()
        .loadTrustMaterial(myTrustStore)
        .build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);

SSLConnectionSocketFactory的定制意味着对SSL / TLS协议的概念有一定程度的熟悉,其详细解释超出了本文档的范围。有关javax.net.ssl.SSLContext和相关工具的详细说明,请参阅Java™安全套接字扩展(JSSE)参考指南。

主机名验证

除了在SSL / TLS协议级别上执行信任验证和客户端身份验证之外,HttpClient还可以选择验证目标主机名是否与存储在服务器X.509证书内的名称匹配。该验证可以提供对服务器信任材料的真实性的附加保证。javax.net.ssl.HostnameVerifier接口表示主机名验证策略。HttpClient提供了两个javax.net.ssl.HostnameVerifier实现。重要提示:主机名验证不应与SSL信任验证混淆。

  • DefaultHostnameVerifier: HttpClient使用的默认实现符合RFC 2818。主机名必须与证书指定的备选名称相匹配,或者在没有给出备选名称的情况下,证书主体的最具体的CN。通配符可以出现在CN和任何主体中。
  • NoopHostnameVerifier: 这个主机名验证者本质上关闭主机名验证。它接受任何有效的SSL会话并匹配目标主机。

默认情况下,HttpClient使用DefaultHostnameVerifier实现。如果需要,可以指定一个不同的主机名验证器实现:

SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslContext,
        NoopHostnameVerifier.INSTANCE);

从版本4.4开始,HttpClient使用由Mozilla基金会友好维护的公共后缀列表,以确保SSL证书中的通配符不会被滥用以应用于具有公共顶级域的多个域。HttpClient附带在发布时检索的列表的副本。列表的最新版本可以在https://publicsuffix.org/list/找到。建议清单的本地副本,并从其原始位置每天下载一次,这是非常值得建议的。

PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(
    PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat"));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);

通过使用空匹配器,可以禁止对公众足迹进行验证。

DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(null);

HTTP代理配置

即使HttpClient知道复杂的路由方案和代理链,它只支持简单的直接或一跳代理连接。

告诉HttpClient通过代理连接到目标主机的最简单的方法是设置默认的代理参数:

HttpHost proxy = new HttpHost("someproxy", 8080);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();

也可以指示HttpClient使用标准JRE代理选择器来获取代理信息:

SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(
        ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();

或者,可以提供自定义的RoutePlanner实现,以完全控制HTTP路由计算过程:

HttpRoutePlanner routePlanner = new HttpRoutePlanner() {

    public HttpRoute determineRoute(
            HttpHost target,
            HttpRequest request,
            HttpContext context) throws HttpException {
        return new HttpRoute(target, null,  new HttpHost("someproxy", 8080),
                "https".equalsIgnoreCase(target.getSchemeName()));
    }

};
CloseableHttpClient httpclient = HttpClients.custom()
        .setRoutePlanner(routePlanner)
        .build();
    }
}

HTTP状态管理

最初,HTTP被设计为一种无状态的,面向请求/响应的协议,它并没有为跨越多个逻辑相关的请求/响应交换的有状态会话做出特殊的规定。随着HTTP协议越来越流行,越来越多的系统开始将其用于应用程序,例如用于电子商务应用程序的传输。因此,对状态管理的支持变成一种必然。

当时,网景通信是一家领先的网络客户端和服务器软件开发商,它们在其产品基于专有规范的基础上实现了对HTTP状态管理的支持。后来,Netscape试图通过发布规范草案来规范这个机制。这些努力有助于通过RFC标准轨道定义的正式规范。但是,大量应用程序中的状态管理仍然主要基于Netscape草案,并且与官方规范不兼容。网络浏览器的所有主要开发者都不得不保持与这些应用程序的兼容性,这些应用程序极大地促成了标准遵从性的分裂。

HTTP cookie是HTTP代理和目标服务器可以交换来维护会话的令牌或短包状态信息。网景工程师曾经把它称为“魔术饼干”。 HttpClient使用Cookie接口来表示一个抽象的cookie标记。HTTP Cookie的最简单的形式就是一个键/值对。通常,HTTP cookie还包含许多属性,例如有效的域,指定应用此cookie的原始服务器上URL的子集的路径以及cookie有效的最长时间。

SetCookie接口表示由原始服务器发送给HTTP代理的一个Set-Cookie响应头,以保持对话状态。

ClientCookie接口扩展了Cookie接口和额外的客户端特定功能,比如能够完全按照原始服务器指定的方式检索原始Cookie属性。这对于生成Cookie标题非常重要,因为某些Cookie规范要求只有在Set-Cookie标头中指定Cookie标头时,Cookie标头才应包含某些属性。

这里是创建一个客户端cookie对象的例子:

BasicClientCookie cookie = new BasicClientCookie("name", "value");
// Set effective domain and path attributes
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
// Set attributes exactly as sent by the server
cookie.setAttribute(ClientCookie.PATH_ATTR, "/");
cookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");

Cookie规范

CookieSpec接口代表一个cookie管理规范。 cookie管理规范预计将执行:

  • 解析Set-Cookie标题的规则
  • 解析Cookie的验证规则
  • 给定的主机,端口和原始路径的Cookie头的格式。

HttpClient附带几个CookieSpec实现:

  • Standard strict: 状态管理策略符合RFC 6265第4节定义的良好行为配置文件的语法和语义。
  • Standard: 状态管理策略符合RFC 6265第4节定义的更为宽松的配置文件,旨在与不符合良好行为的配置文件的现有服务器进行互操作。
  • Netscape draft (obsolete): 此策咯符合Netscape Communications公布的原始草案规范。除非绝对有必要与遗留代码兼容,否则应该避免。
  • RFC 2965 (obsolete): 状态管理策略符合RFC 2965定义的过时状态管理规范。请不要在新的应用程序中使用。
  • RFC 2109 (obsolete): 状态管理策略符合RFC 2109定义的过时状态管理规范。请不要在新的应用程序中使用。
  • Browser compatibility (obsolete): 该策略致力于模仿老版本浏览器应用程序(如Microsoft Internet Explorer和Mozilla FireFox)的(错误)行为。请不要在新的应用程序中使用。
  • Default: 默认的cookie策略是一种综合的策略,根据HTTP响应发送的cookie的属性(如版本属性,现在已经过时),选择符合RFC 2965,RFC 2109或Netscape草案的兼容实现。在下一个次版本的HttpClient中,这个策略将被弃用,以支持标准(符合RFC 6265)实现。
  • Ignore cookies: 所有的cookies都被忽略。

强烈建议在新应用程序中使用标准或严格的严格策略。过时的规格只能用于与旧系统的兼容性。对于过时的规范的支持将在下一个主要版本的HttpClient中被删除。

选择Cookie策略

可以在HTTP客户端上设置Cookie策略,并根据需要在HTTP请求级别上重写Cookie策略。

RequestConfig globalConfig = RequestConfig.custom()
        .setCookieSpec(CookieSpecs.DEFAULT)
        .build();
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultRequestConfig(globalConfig)
        .build();
RequestConfig localConfig = RequestConfig.copy(globalConfig)
        .setCookieSpec(CookieSpecs.STANDARD_STRICT)
        .build();
HttpGet httpGet = new HttpGet("/");
httpGet.setConfig(localConfig);

自定义Cookie策略

为了实现自定义Cookie策略,应该创建一个CookieSpec接口的自定义实现,创建一个CookieSpecProvider实现来创建和初始化自定义规范的实例,并用HttpClient注册工厂。一旦自定义规范已经注册,就可以像标准cookie规范一样激活它。

PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();

Registry r = RegistryBuilder.create()
        .register(CookieSpecs.DEFAULT,
                new DefaultCookieSpecProvider(publicSuffixMatcher))
        .register(CookieSpecs.STANDARD,
                new RFC6265CookieSpecProvider(publicSuffixMatcher))
        .register("easy", new EasySpecProvider())
        .build();

RequestConfig requestConfig = RequestConfig.custom()
        .setCookieSpec("easy")
        .build();

CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultCookieSpecRegistry(r)
        .setDefaultRequestConfig(requestConfig)
        .build();

Cookie持久性

HttpClient可以使用实现CookieStore接口的持久性cookie存储的任何物理表示。名为BasicCookieStore的默认CookieStore实现是一个由java.util.ArrayList支持的简单实现。存储在BasicClientCookie对象中的cookie在容器对象被垃圾收集时会丢失。用户可以根据需要提供更复杂的实现。

// Create a local instance of cookie store
CookieStore cookieStore = new BasicCookieStore();
// Populate cookies if needed
BasicClientCookie cookie = new BasicClientCookie("name", "value");
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
cookieStore.addCookie(cookie);
// Set the store
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultCookieStore(cookieStore)
        .build();

HTTP状态管理和执行上下文

在HTTP请求执行过程中,HttpClient将以下与状态管理相关的对象添加到执行上下文中:

  • Lookup 代表实际的cookie规范注册表的实例。在本地上下文中设置的这个属性的值优先于默认值。
  • CookieSpec 代表实际cookie规范的实例。
  • CookieOrigin 实例代表原始服务器的实际细节。
  • CookieStore 代表实际cookie存储的实例。在本地上下文中设置的这个属性的值优先于默认值。

本地HttpContext对象可用于在请求执行之前自定义HTTP状态管理上下文,或在请求执行后检查其状态。也可以使用单独的执行上下文来实现每个用户(或每个线程)的状态管理。在本地上下文中定义的cookie规范注册表和cookie存储优先于在HTTP客户端级别设置的默认规则。

CloseableHttpClient httpclient = <...>

Lookup cookieSpecReg = <...>
CookieStore cookieStore = <...>

HttpClientContext context = HttpClientContext.create();
context.setCookieSpecRegistry(cookieSpecReg);
context.setCookieStore(cookieStore);
HttpGet httpget = new HttpGet("http://somehost/");
CloseableHttpResponse response1 = httpclient.execute(httpget, context);
<...>
// Cookie origin details
CookieOrigin cookieOrigin = context.getCookieOrigin();
// Cookie spec used
CookieSpec cookieSpec = context.getCookieSpec();

HTTP身份验证

HttpClient完全支持由HTTP标准规范定义的认证方案以及许多广泛使用的非标准认证方案,如NTLM和SPNEGO。

用户凭证

任何用户身份验证过程都需要一组可用于建立用户身份的凭证。最简单的形式是用户凭证可以只是一个用户名/密码对。

UsernamePasswordCredentials表示由明文形式的安全主体和密码组成的一组凭证。这个实现对于HTTP标准规范定义的标准认证方案是足够的。

UsernamePasswordCredentials creds = new UsernamePasswordCredentials("user", "pwd");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

// 输出
user
pwd

NTCredentials是一个特定于Microsoft Windows的实现,除了用户名/密码对之外,还包括一组额外的Windows特定属性,例如用户域的名称。在Microsoft Windows网络中,同一用户可以属于多个域,每个域都有一组不同的授权。

NTCredentials creds = new NTCredentials("user", "pwd", "workstation", "domain");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());

// 输出
DOMAIN/user
pwd

认证方案

AuthScheme接口表示抽象的面向质询 - 响应的认证方案。认证方案将支持以下功能:

  • 解析和处理目标服务器发送的质询,以响应受保护资源的请求。
  • 提供处理后的挑战的属性:认证方案类型及其参数,如认证方案适用的领域(如果可用)
  • 为给定的一组凭证和HTTP请求生成授权字符串以响应实际的授权质询。

请注意,身份验证方案可能是有状态的,质询一系列挑战 - 响应交换。

HttpClient附带有几个AuthScheme实现:

  • Basic: RFC 2617中定义的基本认证方案。这种认证方案是不安全的,因为凭证是以明文形式传输的。尽管不安全基本身份验证方案如果与TLS / SSL加密结合使用,则完全可以满足要求。
  • Digest: 摘要认证方案在RFC 2617中定义。摘要式身份验证方案比Basic更安全,对于那些不希望通过TLS / SSL加密实现完全传输安全性开销的应用程序来说,它可能是一个不错的选择。
  • NTLM: NTLM是Microsoft开发的专用身份验证方案,针对Windows平台进行了优化。 NTLM被认为比Digest更安全。
  • SPNEGO: SPNEGO(简单和受保护的GSSAPI协商机制)是一种GSSAPI“伪机制”,用于协商许多可能的实际机制之一。SPNEGO最明显的用途是在Microsoft的HTTP协商认证扩展中。可协商的子机制包括由Active Directory支持的NTLM和Kerberos。目前HttpClient只支持Kerberos子机制。
  • Kerberos: Kerberos身份验证实现。

凭证供应商

凭证提供程序旨在维护一组用户凭证,并能够为特定的认证范围生成用户凭证。身份验证范围由主机名,端口号,领域名称和身份验证方案名称组成。当向凭证提供者注册凭证时,可以提供通配符(任何主机,任何端口,任何领域,任何方案)而不是具体的属性值。如果无法找到直接匹配,凭证提供程序将希望能够找到特定范围的最接近的匹配项。
HttpClient可以处理实现CredentialsProvider接口的凭证提供者的任何物理表示。名为BasicCredentialsProvider的默认CredentialsProvider实现是一个由java.util.HashMap支持的简单实现。

CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
    new AuthScope("somehost", AuthScope.ANY_PORT), 
    new UsernamePasswordCredentials("u1", "p1"));
credsProvider.setCredentials(
    new AuthScope("somehost", 8080), 
    new UsernamePasswordCredentials("u2", "p2"));
credsProvider.setCredentials(
    new AuthScope("otherhost", 8080, AuthScope.ANY_REALM, "ntlm"), 
    new UsernamePasswordCredentials("u3", "p3"));

System.out.println(credsProvider.getCredentials(
    new AuthScope("somehost", 80, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("somehost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("otherhost", 8080, "realm", "basic")));
System.out.println(credsProvider.getCredentials(
    new AuthScope("otherhost", 8080, null, "ntlm")));

// 输出
[principal: u1]
[principal: u2]
null
[principal: u3]

HTTP认证和执行上下文

HttpClient依靠AuthState类来跟踪有关身份验证过程的详细信息。HttpClient在HTTP请求执行过程中创建两个AuthState实例:一个用于目标主机身份验证,另一个用于代理身份验证。如果目标服务器或代理需要用户身份验证,则相应的AuthScope实例将使用在身份验证过程中使用的AuthScope,AuthScheme和Crednetials来填充。可以检查AuthState以查明请求的身份验证类型,是否找到匹配的AuthScheme实现以及凭证提供程序是否设法找到给定身份验证范围的用户凭证。

在HTTP请求执行过程中,HttpClient将以下与认证相关的对象添加到执行上下文中:

  • Lookup 表示实际认证方案注册表的实例。在本地上下文中设置的这个属性的值优先于默认值。
  • CredentialsProvider 表示实际凭据提供者的实例。在本地上下文中设置的这个属性的值优先于默认值。
  • AuthState 表示实际目标验证状态的实例。在本地上下文中设置的这个属性的值优先于默认值。
  • AuthState 表示实际代理身份验证状态的实例。在本地上下文中设置的这个属性的值优先于默认值。
  • AuthCache 表示实际认证数据缓存的实例。在本地上下文中设置的这个属性的值优先于默认值。

本地HttpContext对象可用于在请求执行之前自定义HTTP认证上下文,或者在请求执行后检查其状态:

CloseableHttpClient httpclient = <...>

CredentialsProvider credsProvider = <...>
Lookup authRegistry = <...>
AuthCache authCache = <...>

HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthSchemeRegistry(authRegistry);
context.setAuthCache(authCache);
HttpGet httpget = new HttpGet("http://somehost/");
CloseableHttpResponse response1 = httpclient.execute(httpget, context);
<...>

AuthState proxyAuthState = context.getProxyAuthState();
System.out.println("Proxy auth state: " + proxyAuthState.getState());
System.out.println("Proxy auth scheme: " + proxyAuthState.getAuthScheme());
System.out.println("Proxy auth credentials: " + proxyAuthState.getCredentials());
AuthState targetAuthState = context.getTargetAuthState();
System.out.println("Target auth state: " + targetAuthState.getState());
System.out.println("Target auth scheme: " + targetAuthState.getAuthScheme());
System.out.println("Target auth credentials: " + targetAuthState.getCredentials());

认证数据的缓存

从版本4.1开始,HttpClient会自动缓存已成功验证的主机信息。请注意,必须使用相同的执行上下文来执行逻辑相关的请求,以便将缓存的认证数据从一个请求传播到另一个请求。执行上下文超出范围后,身份验证数据将立即丢失。

抢先认证(Preemptive authentication)

HttpClient不支持开箱即用的认证,因为如果误用或使用不当,抢先认证可能导致重大的安全问题,例如以明文形式将用户凭据发送给未经授权的第三方。因此,希望用户在特定的应用环境中评估抢先认证与安全风险的潜在益处。

但是可以通过预先填充认证数据缓存来配置HttpClient进行抢先认证。

CloseableHttpClient httpclient = <...>

HttpHost targetHost = new HttpHost("localhost", 80, "http");
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
        new AuthScope(targetHost.getHostName(), targetHost.getPort()),
        new UsernamePasswordCredentials("username", "password"));

// Create AuthCache instance
AuthCache authCache = new BasicAuthCache();
// Generate BASIC scheme object and add it to the local auth cache
BasicScheme basicAuth = new BasicScheme();
authCache.put(targetHost, basicAuth);

// Add AuthCache to the execution context
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);
context.setAuthCache(authCache);

HttpGet httpget = new HttpGet("/");
for (int i = 0; i < 3; i++) {
    CloseableHttpResponse response = httpclient.execute(
            targetHost, httpget, context);
    try {
        HttpEntity entity = response.getEntity();

    } finally {
        response.close();
    }
}

NTLM身份验证

从版本4.1开始,HttpClient提供对NTLMv1,NTLMv2和NTLM2会话认证的全面支持。人们仍然可以继续使用由Samba项目开发的外部NTLM引擎(如JCIFS库)作为其Windows互操作性套件程序的一部分。

NTLM连接持久性

与标准的基本和摘要方案相比,NTLM认证方案在计算开销和性能影响方面明显更昂贵。这可能是微软选择使NTLM身份验证方案处于有状态的主要原因之一。也就是说,一旦通过身份验证,用户身份就与该连接的整个使用期限相关联。NTLM连接的有状态性使得连接持久性更加复杂,因为持久性NTLM连接的明显原因可能不会被具有不同用户身份的用户重新使用。HttpClient附带的标准连接管理器完全能够管理有状态的连接。然而,在同一个会话中,逻辑上相关的请求使用相同的执行上下文以使他们知道当前的用户身份是非常重要的。否则,HttpClient将最终为每个针对NTLM保护资源的HTTP请求创建一个新的HTTP连接。有关有状态HTTP连接的详细讨论,请参阅本节。

由于NTLM连接是有状态的,因此通常建议使用相对便宜的方法来触发NTLM身份验证,如GET或HEAD,并重新使用相同的连接执行更昂贵的方法,特别是那些包含请求实体(如POST或PUT)的方法。

CloseableHttpClient httpclient = <...>

CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(AuthScope.ANY,
        new NTCredentials("user", "pwd", "myworkstation", "microsoft.com"));

HttpHost target = new HttpHost("www.microsoft.com", 80, "http");

// Make sure the same context is used to execute logically related requests
HttpClientContext context = HttpClientContext.create();
context.setCredentialsProvider(credsProvider);

// Execute a cheap method first. This will trigger NTLM authentication
HttpGet httpget = new HttpGet("/ntlm-protected/info");
CloseableHttpResponse response1 = httpclient.execute(target, httpget, context);
try {
    HttpEntity entity1 = response1.getEntity();
} finally {
    response1.close();
}

// Execute an expensive method next reusing the same context (and connection)
HttpPost httppost = new HttpPost("/ntlm-protected/form");
httppost.setEntity(new StringEntity("lots and lots of data"));
CloseableHttpResponse response2 = httpclient.execute(target, httppost, context);
try {
    HttpEntity entity2 = response2.getEntity();
} finally {
    response2.close();
}

SPNEGO / Kerberos身份验证

SPNEGO(简单和受保护的GSSAPI协商机制)被设计为当两端都不知道对方能够使用/提供什么时,允许对服务进行认证。这是最常用的Kerberos身份验证。它可以包装其他机制,但是HttpClient中的当前版本仅仅考虑了Kerberos。

HttpClient对SPNEGO的支持

SPNEGO身份验证方案与Sun Java 1.5及更高版本兼容。但强烈建议使用Java> = 1.6,因为它更完整地支持SPNEGO身份验证。Sun JRE提供了几乎所有的Kerberos和SPNEGO令牌处理的支持类。这意味着很多设置是针对GSS类的。 SPNegoScheme是一个简单的类来处理令牌的编组和读写正确的头文件。

最好的方法是在示例中获取KerberosHttpClient.java文件,并尝试使其运行。有很多问题可以发生,但如果幸运的话,它会工作,没有太多的问题。它也应该提供一些输出来调试。

在Windows中,它应该默认使用登录的凭据;这可以通过使用例如’kinit’来覆盖。 $ JAVA_HOME \ bin \ kinit [email protected],这对测试和调试问题非常有帮助。删除由kinit创建的缓存文件,以恢复到Windows Kerberos缓存。

确保在krb5.conf文件中列出domain_realms。这是问题的主要来源。

GSS / Java Kerberos安装程序

本文档假设您使用的是Windows,但大部分信息也适用于Unix。org.ietf.jgss类有很多可能的配置参数,主要是在krb5.conf / krb5.ini文件中。有关http://web.mit.edu/kerberos/krb5-1.4/krb5-1.4.1/doc/krb5-admin/krb5.conf.html格式的更多信息。

  • Client Web Browser does HTTP GET for resource.
  • Web server returns HTTP 401 status and a header: WWW-Authenticate: Negotiate
  • Client generates a NegTokenInit, base64 encodes it, and resubmits the GET with an Authorization header: Authorization: Negotiate .
  • Server decodes the NegTokenInit, extracts the supported MechTypes (only Kerberos V5 in our case), ensures it is one of the expected ones, and then extracts the MechToken (Kerberos Token) and authenticates it.
    If more processing is required another HTTP 401 is returned to the client with more data in the the WWW-Authenticate header. Client takes the info and generates another token passing this back in the Authorization header until complete.
  • When the client has been authenticated the Web server should return the HTTP 200 status, a final WWW-Authenticate header and the page content.

login.conf 文件

以下配置是在Windows XP中针对IIS和JBoss协商模块的基本设置。
系统属性java.security.auth.login.config可以用来指向login.conf文件。

login.conf内容可能如下所示:

com.sun.security.jgss.login {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

com.sun.security.jgss.initiate {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

com.sun.security.jgss.accept {
  com.sun.security.auth.module.Krb5LoginModule required client=TRUE useTicketCache=true;
};

krb5.conf / krb5.ini文件

如果未指定,将使用系统默认值。通过设置系统属性java.security.krb5.conf指向一个自定义的krb5.conf文件来覆盖(如果需要的话)。 krb5.conf的内容可能如下所示:

[libdefaults]
    default_realm = AD.EXAMPLE.NET
    udp_preference_limit = 1
[realms]
    AD.EXAMPLE.NET = {
        kdc = KDC.AD.EXAMPLE.NET
    }
[domain_realms]
.ad.example.net=AD.EXAMPLE.NET
ad.example.net=AD.EXAMPLE.NET

Windows特定配置

要允许Windows使用当前用户的票据,必须将系统属性javax.security.auth.useSubjectCredsOnly设置为false,并且应该添加并正确设置Windows注册表项allowtgtsessionkey,以允许在Kerberos票证授予票证中发送会话密钥。

在Windows Server 2003和Windows 2000 SP4上,这里是所需的注册表设置:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters
Value Name: allowtgtsessionkey
Value Type: REG_DWORD
Value: 0x01

以下是Windows XP SP2中注册表设置的位置:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\
Value Name: allowtgtsessionkey
Value Type: REG_DWORD
Value: 0x01

流API(fluent api)

易于使用的API

从4.2版开始,HttpClient基于流畅的界面概念提供了一个易于使用的Facade API。Fluent Facade API只公开了HttpClient的最基本的功能,并且适用于不需要HttpClient的全部灵活性的简单用例。例如,流畅的Facade API可以让用户不必处理连接管理和资源释放。

以下是通过HC fluent API执行的HTTP请求的几个示例:

// Execute a GET with timeout settings and return response content as String.
Request.Get("http://somehost/")
        .connectTimeout(1000)
        .socketTimeout(1000)
        .execute().returnContent().asString();
// Execute a POST with the 'expect-continue' handshake, using HTTP/1.1,
// containing a request body as String and return response content as byte array.
Request.Post("http://somehost/do-stuff")
        .useExpectContinue()
        .version(HttpVersion.HTTP_1_1)
        .bodyString("Important stuff", ContentType.DEFAULT_TEXT)
        .execute().returnContent().asBytes();
// Execute a POST with a custom header through the proxy containing a request body
// as an HTML form and save the result to the file
Request.Post("http://somehost/some-form")
        .addHeader("X-Custom-header", "stuff")
        .viaProxy(new HttpHost("myproxy", 8080))
        .bodyForm(Form.form().add("username", "vip").add("password", "secret").build())
        .execute().saveContent(new File("result.dump"));

还可以直接使用Executor来执行特定安全上下文中的请求,从而将身份验证信息缓存起来并重新用于后续请求。

Executor executor = Executor.newInstance()
        .auth(new HttpHost("somehost"), "username", "password")
        .auth(new HttpHost("myproxy", 8080), "username", "password")
        .authPreemptive(new HttpHost("myproxy", 8080));

executor.execute(Request.Get("http://somehost/"))
        .returnContent().asString();

executor.execute(Request.Post("http://somehost/do-stuff")
        .useExpectContinue()
        .bodyString("Important stuff", ContentType.DEFAULT_TEXT))
        .returnContent().asString();

响应处理

流畅的Facade API通常使用户不必处理连接管理和资源释放。但是在大多数情况下,这是以不得不缓冲内存中响应消息的内容为代价的。强烈建议使用ResponseHandler进行HTTP响应处理,以避免在内存中缓冲内容。

Document result = Request.Get("http://somehost/content")
        .execute().handleResponse(new ResponseHandler() {

    public Document 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");
        }
        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
            ContentType contentType = ContentType.getOrDefault(entity);
            if (!contentType.equals(ContentType.APPLICATION_XML)) {
                throw new ClientProtocolException("Unexpected content type:" +
                    contentType);
            }
            String charset = contentType.getCharset();
            if (charset == null) {
                charset = HTTP.DEFAULT_CONTENT_CHARSET;
            }
            return docBuilder.parse(entity.getContent(), charset);
        } catch (ParserConfigurationException ex) {
            throw new IllegalStateException(ex);
        } catch (SAXException ex) {
            throw new ClientProtocolException("Malformed XML document", ex);
        }
    }

    });

HTTP缓存

一般概念

HttpClient Cache提供了一个与HTTP / 1.1兼容的缓存层,可以和HttpClient一起使用 - 这是Java浏览器缓存的等价物。该实现遵循责任链设计模式,其中缓存HttpClient实现可以为默认的非缓存HttpClient实现提供一个嵌入式替代; 完全可以从缓存满足的请求不会导致实际的原始请求。使用条件GET和If-Modified-Since和/或If-None-Match请求标头,过期的缓存条目将尽可能使用原始地址进行自动验证。

一般而言,HTTP / 1.1缓存被设计为在语义上是透明的;也就是说,缓存不应该改变客户端和服务器之间请求 - 响应交换的含义。因此,将缓存的HttpClient放置到现有的兼容客户端 - 服务器关系中应该是安全的。虽然缓存模块是从HTTP协议的角度来看是客户端的一部分,但实现的目标是与透明缓存代理的要求兼容。

最后,缓存HttpClient包括支持由RFC 5861指定的缓存控制扩展(stale-if-error和stale-while-revalidate)。

当缓存HttpClient执行请求时,它会经历以下流程:

  • 检查基本符合HTTP 1.1协议的请求,并尝试更正请求。
  • 刷新任何将被此请求废除的缓存条目。
  • 确定当前请求是否可以从缓存中获取。如果不是,则直接将请求传递给原始服务器,并在适当的情况下缓存后返回响应。
  • 如果这是一个缓存可服务请求,它将尝试从缓存中读取它。如果不在缓存中,则调用原始服务器并缓存响应(如果适用)。
  • 如果缓存的响应适合作为响应,则构造包含ByteArrayEntity的BasicHttpResponse并将其返回。否则,尝试对原始服务器重新验证缓存项。
  • 对于无法重新验证的缓存响应,请调用原始服务器并缓存响应(如果适用)。

当缓存HttpClient收到一个响应,它会经历以下流程:

  • 检查协议符合性的响应
  • 确定响应是否可缓存
  • 如果它是可缓存的,则尝试读取配置中允许的最大大小并将其存储在缓存中。
  • 如果缓存的响应太大,请重新构建部分消耗的响应,并直接返回而不缓存。

请注意,缓存HttpClient本身不是HttpClient的不同实现,而是通过将自身作为附加处理组件插入到请求执行管道来工作。

RFC-2616合规性

我们相信HttpClient缓存是无条件符合RFC-2616的。也就是说,无论规范如何指示,必须,不应该,或者不应该为HTTP缓存,缓存层试图以满足这些要求的方式行事。这意味着缓存模块在放入时不会产生不正确的行为。

用法示例

这是如何设置一个基本的缓存HttpClient的简单例子。按照配置,它将最多存储1000个缓存对象,每个对象的最大主体大小为8192字节。这里选择的数字仅仅是举例而已,并不是要说明性的或者被认为是建议。

CacheConfig cacheConfig = CacheConfig.custom()
        .setMaxCacheEntries(1000)
        .setMaxObjectSize(8192)
        .build();
RequestConfig requestConfig = RequestConfig.custom()
        .setConnectTimeout(30000)
        .setSocketTimeout(30000)
        .build();
CloseableHttpClient cachingClient = CachingHttpClients.custom()
        .setCacheConfig(cacheConfig)
        .setDefaultRequestConfig(requestConfig)
        .build();

HttpCacheContext context = HttpCacheContext.create();
HttpGet httpget = new HttpGet("http://www.mydomain.com/content/");
CloseableHttpResponse response = cachingClient.execute(httpget, context);
try {
    CacheResponseStatus responseStatus = context.getCacheResponseStatus();
    switch (responseStatus) {
        case CACHE_HIT:
            System.out.println("A response was generated from the cache with " +
                    "no requests sent upstream");
            break;
        case CACHE_MODULE_RESPONSE:
            System.out.println("The response was generated directly by the " +
                    "caching module");
            break;
        case CACHE_MISS:
            System.out.println("The response came from an upstream server");
            break;
        case VALIDATED:
            System.out.println("The response was generated from the cache " +
                    "after validating the entry with the origin server");
            break;
    }
} finally {
    response.close();
}

配置

缓存HttpClient继承了默认非缓存实现(包括设置选项,如超时和连接池大小)的所有配置选项和参数。对于特定于缓存的配置,可以提供CacheConfig实例来自定义以下区域的行为:

  • 缓存大小。如果后端存储支持这些限制,则可以指定最大缓存条目数以及最大可缓存响应主体大小。
  • 公/私人缓存。默认情况下,缓存模块认为自己是一个共享(公共)缓存,并且不会缓存对具有“缓存控制:私人”标记的授权头或响应的请求的响应。但是,如果高速缓存仅由一个逻辑“用户”(与浏览器高速缓存类似)使用,那么您将需要关闭共享高速缓存设置。
  • 启发式缓存。根据RFC2616,即使没有明确的缓存控制头由原始设置,缓存也可以缓存某些缓存条目。这种行为在默认情况下是关闭的,但是如果您正在使用没有设置正确标题但您仍然想要缓存响应的源,则可能需要将其打开。您将希望启用启发式缓存,然后指定自上次修改资源以来的默认新鲜度生存期和/或时间的一小部分。有关启发式缓存的更多详细信息,请参阅HTTP / 1.1 RFC的第13.2.2和13.2.4节。
  • 后台验证。缓存模块支持RFC5861的stale-while-revalidate指令,允许在后台发生某些缓存条目重新验证。您可能需要调整后台工作线程的最小和最大数量的设置,以及在回收之前它们可以空闲的最长时间。当没有足够的工作线程跟上需求时,您还可以控制用于重新验证的队列大小。

存储后端

缓存HttpClient的默认实现将缓存条目和缓存的响应实体存储在应用程序的JVM的内存中。虽然这提供了很高的性能,但是由于大小的限制,或者由于缓存条目是短暂的,并且在应用程序重新启动时不能存活,所以可能不适合您的应用程序。当前版本包括支持使用EhCache和memcached实现存储缓存条目,这允许将缓存条目溢出到磁盘或将其存储在外部进程中。

如果这些选项都不适合您的应用程序,那么可以通过实现HttpCacheStorage接口来提供您自己的存储后端,然后在构建时提供它来缓存HttpClient。在这种情况下,缓存条目将使用您的方案进行存储,但您将重用所有关于HTTP / 1.1遵从性和缓存处理的逻辑。一般来说,应该可以使用支持键/值存储(类似于Java Map接口)的任何东西来创建HttpCacheStorage实现,并能够应用原子更新。

最后,通过一些额外的努力,完全有可能建立一个多层缓存层次结构; 例如,将内存缓存HttpClient包装在磁盘上或远程存储在缓存中的缓存条目中,遵循类似于虚拟内存,L1 / L2处理器缓存等的模式。

高级主题

自定义客户端连接

在某些情况下,为了能够处理非标准,不符合规范的行为,可能有必要定制HTTP消息通过线路传输的方式,超出了使用HTTP参数进行传输的方式。例如,对于Web爬虫,可能需要强制HttpClient接受格式不正确的响应头以挽救消息的内容。

通常,插入自定义消息解析器或自定义连接实现的过程涉及以下几个步骤:

  • 提供一个自定义的LineParser / LineFormatter接口实现。根据需要实现消息解析/格式化逻辑。
class MyLineParser extends BasicLineParser {

    @Override
    public Header parseHeader(
            CharArrayBuffer buffer) throws ParseException {
        try {
            return super.parseHeader(buffer);
        } catch (ParseException ex) {
            // Suppress ParseException exception
            return new BasicHeader(buffer.toString(), null);
        }
    }

}
  • 提供一个自定义的HttpConnectionFactory实现。将默认的请求书写器和/或响应解析器替换为自定义的。
HttpConnectionFactory connFactory =
        new ManagedHttpClientConnectionFactory(
            new DefaultHttpRequestWriterFactory(),
            new DefaultHttpResponseParserFactory(
                    new MyLineParser(), new DefaultHttpResponseFactory()));
  • 配置HttpClient以使用自定义连接工厂。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
    connFactory);
CloseableHttpClient httpclient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

有状态的HTTP连接

虽然HTTP规范假定会话状态信息总是以HTTP cookie的形式嵌入到HTTP消息中,因此HTTP连接总是无状态的,但这种假设在现实生活中并不总是成立。在某些情况下,HTTP连接是使用特定的用户身份或在特定的安全上下文中创建的,因此不能与其他用户共享,并且只能由同一用户重用。这种有状态的HTTP连接的示例是NTLM身份验证连接和带有客户端证书身份验证的SSL连接。

用户令牌处理程序

HttpClient依靠UserTokenHandler接口来确定给定的执行上下文是否是用户特定的。如果上下文是用户特定的,则该处理程序返回的标记对象应该唯一标识当前用户;如果上下文不包含任何特定于当前用户的资源或细节,则为null。用户令牌将用于确保用户特定资源不会与其他用户共享或重新使用。

UserTokenHandler接口的默认实现使用Principal类的实例来表示HTTP连接的状态对象(如果可以从给定的执行上下文中获取的话)。DefaultUserTokenHandler将使用基于连接的身份验证方案的用户主体,如NTLM或SSL会话的客户端身份验证打开。如果两者都不可用,则返回空令牌。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context = HttpClientContext.create();
HttpGet httpget = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response = httpclient.execute(httpget, context);
try {
    Principal principal = context.getUserToken(Principal.class);
    System.out.println(principal);
} finally {
    response.close();
}

如果默认的用户不满足他们的需求,用户可以提供一个自定义的实现:

UserTokenHandler userTokenHandler = new UserTokenHandler() {

    public Object getUserToken(HttpContext context) {
        return context.getAttribute("my-token");
    }

};
CloseableHttpClient httpclient = HttpClients.custom()
        .setUserTokenHandler(userTokenHandler)
        .build();

持久的有状态连接

请注意,只有在执行请求时将相同的状态对象绑定到执行上下文,才能重新使用携带状态对象的持久连接。因此,确保相同的上下文被同一用户的后续HTTP请求的执行重用,或者在请求执行之前用户令牌被绑定到上下文是非常重要的。

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpClientContext context1 = HttpClientContext.create();
HttpGet httpget1 = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response1 = httpclient.execute(httpget1, context1);
try {
    HttpEntity entity1 = response1.getEntity();
} finally {
    response1.close();
}
Principal principal = context1.getUserToken(Principal.class);

HttpClientContext context2 = HttpClientContext.create();
context2.setUserToken(principal);
HttpGet httpget2 = new HttpGet("http://localhost:8080/");
CloseableHttpResponse response2 = httpclient.execute(httpget2, context2);
try {
    HttpEntity entity2 = response2.getEntity();
} finally {
    response2.close();
}

使用FutureRequestExecutionService

使用FutureRequestExecutionService,您可以调度http调用,并将响应视为未来。这是有用的,例如,多次调用Web服务。使用FutureRequestExecutionService的优点是可以使用多个线程同时调度请求,设置任务超时或在不再需要响应时取消它们。

FutureRequestExecutionService使用扩展FutureTask的HttpRequestFutureTask封装请求。这个类允许您取消任务,并跟踪各种指标,如请求持续时间。

创建FutureRequestExecutionService

futureRequestExecutionService的构造函数接受任何现有的httpClient实例和一个ExecutorService实例。配置两者时,重要的是将最大连接数与要使用的线程数对齐。当线程比连接多时,连接可能开始超时,因为没有可用的连接。当连接比线程多,futureRequestExecutionService将不会使用所有它们。

HttpClient httpClient = HttpClientBuilder.create().setMaxConnPerRoute(5).build();
ExecutorService executorService = Executors.newFixedThreadPool(5);
FutureRequestExecutionService futureRequestExecutionService =
    new FutureRequestExecutionService(httpClient, executorService);

计划请求

要安排请求,只需提供一个HttpUriRequest,HttpContext和一个ResponseHandler。由于请求由执行程序服务处理,所以必须使用ResponseHandler。

private final class OkidokiHandler implements ResponseHandler<Boolean> {
    public Boolean handleResponse(
            final HttpResponse response) throws ClientProtocolException, IOException {
        return response.getStatusLine().getStatusCode() == 200;
    }
}

HttpRequestFutureTask task = futureRequestExecutionService.execute(
    new HttpGet("http://www.google.com"), HttpClientContext.create(),
    new OkidokiHandler());
// blocks until the request complete and then returns true if you can connect to Google
boolean ok=task.get();

取消任务

计划任务可能会被取消。如果任务尚未执行,但只是排队等待执行,它将永远不会执行。如果正在执行并且mayInterruptIfRunning参数设置为true,则会在请求上调用abort()否则响应将被忽略,但请求将被允许正常完成。对task.get()的任何后续调用都将失败,并显示IllegalStateException。应该注意的是取消任务只是释放客户端资源。该请求实际上可以在服务器端正常处理。

task.cancel(true)
task.get() // throws an Exception

回调

可以使用FutureCallback实例而不是手动调用task.get(),在请求完成时获取回调。这与HttpAsyncClient中使用的接口相同。

private final class MyCallback implements FutureCallback<Boolean> {

    public void failed(final Exception ex) {
        // do something
    }

    public void completed(final Boolean result) {
        // do something
    }

    public void cancelled() {
        // do something
    }
}

HttpRequestFutureTask task = futureRequestExecutionService.execute(
    new HttpGet("http://www.google.com"), HttpClientContext.create(),
    new OkidokiHandler(), new MyCallback());

度量(metrics)

FutureRequestExecutionService通常用于进行大量Web服务调用的应用程序中。为了便于例如监视或配置调整,FutureRequestExecutionService跟踪几个指标。每个HttpRequestFutureTask都提供了获取任务计划,启动和结束时间的方法。另外,请求和任务持续时间也是可用的。这些指标汇总在FutureRequestExecutionMetrics实例中的FutureRequestExecutionService中,该实例可以通过FutureRequestExecutionService.metrics()进行访问。

task.scheduledTime() // returns the timestamp the task was scheduled
task.startedTime() // returns the timestamp when the task was started
task.endedTime() // returns the timestamp when the task was done executing
task.requestDuration // returns the duration of the http request
task.taskDuration // returns the duration of the task from the moment it was scheduled

FutureRequestExecutionMetrics metrics = futureRequestExecutionService.metrics()
metrics.getActiveConnectionCount() // currently active connections
metrics.getScheduledConnectionCount(); // currently scheduled connections
metrics.getSuccessfulConnectionCount(); // total number of successful requests
metrics.getSuccessfulConnectionAverageDuration(); // average request duration
metrics.getFailedConnectionCount(); // total number of failed tasks
metrics.getFailedConnectionAverageDuration(); // average duration of failed tasks
metrics.getTaskCount(); // total number of tasks scheduled
metrics.getRequestCount(); // total number of requests
metrics.getRequestAverageDuration(); // average request duration
metrics.getTaskAverageDuration(); // average task duration

希望大家留言评论指正!
让这篇教程译文越来越标准,可读性更强!帮助更多的人!
感谢!


[1]:官方教程原文链接 https://hc.apache.org/httpcomponents-client-4.5.x/tutorial/html/index.html


2018年8月11日,今天是我的生日。
在今天,开始自己的技术博客生涯。
坚持!

你可能感兴趣的:(Java)