赞
踩
Apache HttpComponents组件是负责创建和管理专注于HTTP和相关协议的低级别Java组件的工具集。
是Apache Software Foundation(ASF:Apache软件基金会)下的功能。
ASF主页:http://www.apache.org/
Apache HttpComponents主页:https://hc.apache.org/
HttpClient不是浏览器。它是客户端HTTP传输类库。HttpClient的目的是传输和接收HTTP消息。HttpClient不会尝试处理内容,执行在HTML页面中嵌入的javascript,尝试猜测内容类型(如果没有显性地设置),重新格式化请求/重写位置URI和其他无关HTTP传输的功能。
HttpCore是低层次HTTP传输协议的组件,它可以用于以最小的占用空间构建自定义Client和Server端HTTP服务。HttpCore支持两种I/O模型:基于常规的Java I/O的阻塞I/O模型,基于Java NIO的事件驱动I/O模型。
HttpClient是一个兼容HTTP/1.1的基于HttpCore的HTTP代理实现。它还为客户端身份认证,HTTP状态管理,HTTP连接管理提供了可重用组件。HttpComponents Client是Commons HttpClient3.x的继承者和替代组件。强烈推荐Commons HttpClient用户升级。
3.x版本不在维护。建议升级到HttpClient4.1或最新版本。
大多数HttpClient的必不可少的功能是执行HTTP方法。HTTP方法的执行涉及一个或者多个HTTP请求/响应交换,通常由HttpClient内部处理。用户需要提供一个要执行的请求对象,HttpClient需要将请求对象传输到目标服务器并返回响应的响应对象,或者如果执行没有成功则抛出一个异常。
很自然地,HttpClient API的主要入口点是定义以上所描述约定的HttpClient接口。
下面是request最简单形式的执行过程的一个示例:
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("http://localhost/");
CloseableHttpResponse response = httpclient.execute(httpget);
try {
<...>
} finally {
response.close();
}
所有的HTTP请求有一个由方法名称,请求URI和HTTP协议版本组成的请求行。
HttpClient支持在HTTP1.1规范定义的所有开箱即用的HTTP方法:GET
,HEAD
,POST
,PUT
,DELETE
,TRACE
和OPTIONS
。对于每一个方法类型有一个特定的类:HttpGet
,HttpHead
,HttpPut
,HttpDelete
,HttpTrace
和HttpOptions
。Request-URI是Uniform Resource Identifier(统一资源标志符),标志应用请求的资源。HTTP请求URI是由协议方案,主机名称,资源路径和可选查询和可选段组成(#myelement)。
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());
stdout>
http://www.google.com/search?q=httpclient&btnG=Google+Search&aq=f&oq=
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());
stdout>
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);
stdout>
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());
}
stdout>
Set-Cookie: c1=a; path=/; domain=localhost
Set-Cookie: c2=b; path="/", c3=c; domain="localhost"
它也提供方便的方法来将HTTP消息转换为单个的头数据元素。
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]); } }
stdout>
c1 = a
path=/
domain=localhost
c2 = b
path=/
c3 = c
domain=localhost
HTTP消息可以带有一个与请求或者响应关联的内容实体。实体可以在一些请求和响应中发现,因为他们是可选的。使用实体的请求被称为实体封装请求。HTTP规范定义两个实体封装请求方法:POST
和PUT
。响应一般需要封装一个内容实体。此规则也存在例外,例如HEAD方法的响应和204 No Content,304 Not Modified,205 Reset Content响应。
HttpClient区分了三种实体,取决于他们的内容起源于哪里:
一个实体可以重复,意味着它的内容可以被读取多次。这仅可以使用自包含实体(像ByteArrayEntity
或者StringEntity
)。
因为一个实体可以同时表示二进制和字符内容,它已经支持字符编码(以支持后者,即字符内容)。
当执行一个请求时创建实体,或者当请求成功并且响应体用于发送结果返回到客户端时创建。
要从实体(entity)中读取内容,可以通过HttpEntity#getContent()
方法检索输入流,它将返回一个java.io.InputStream
,或者可以提供一个输出流到HttpEntity#writeTo(OutputStream)
方法,它将在所有内容写入到给定的流后返回。
当实体收到一个传入的消息时,方法HttpEntity#getContentType()
和HttpEntity#getContentLength()
方法可以用于读取通用的元数据,例如Content-Type
和Content-Length
头数据(如果他们有效)。因为Content-Type
头数据可以包含一个用于文本文件类型的字符编码,就像text/plain或者text/html,HttpEntity#getContentEncoding()
方法是用于读取此信息。如果头数据不可用,将返回-1,并且内容类型为空。如果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);
stdout>
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连接高效输出内容的几个类。这些类的实体可以和可以与实体封装请求相关联,例如POST和PUT,以将实体内容封装到输出HTTP请求中。HttpClient提供了用于最常用的数据容器的几个类,例如字符串(string),字节数组(byte array),输入流(input stream)和文件(file):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表单的过程,例如,为了登陆web应用程序或者提交input数据。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=value1m2=value2
HTTP/1.1支持,请求头Transfer-Encoding为chunked
通常建议让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
接口,其包含handResponse(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);
对于HTTP请求的执行HttpClient
接口表示最必不可少的契约。它没有对请求执行过程施加任何限制和特性细节,而是将链接管理,状态管理,身份认证和重定向处理的细节留给各个实现。这将更容易使用额外的功能装饰接口,例如响应内容缓存。
通常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
实现应该是线程安全的。推荐此类相同的实例被重用于多个request执行。
当不在需要CloseableHttpClient
并且已经超出作用域时,必须调用CloseableHttpClient#close()
方法关闭与它关联的链接管理器。
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
<...>
} finally {
httpclient.close();
}
最初,HTTP被设计为无状态,面向响应请求的协议。但是实际应用程序通常需要通过多个请求相应交换来持久存储状态信息。为了使应用程序维护一个过程状态,HttpClient允许Http请求在一个特定的执行上下文被执行,被成为HTTP上下文。如果相同的上下文在连续的请求之间被重用,多个逻辑相关的请求可以参与到一个逻辑会话。HTTP上下文功能类似于java.util.Map<String,Object>
。它只是任意命名value的集合。一个应用程序可以在请求执行之前填充上下文属性或者执行已经完成后检查上下文。
HTTP请求执行的过程中,HttpClient添加以下属性到执行上下文:
HttpConnection
实例表示目标服务器的实际连接。HttpHost
实例表示连接目标。HttpRoute
实例表示完成的连接路由。HttpRequest
实例表示实际的HTTP请求。在执行上下文中的最终HttpRequest
对象精确地表示消息的状态,因为它被发送到目标服务器。每个默认的HTTP/1.0和HTTP/1.1使用相对的请求URI,但是如果请求通过使用非隧道代理发送,URI将是绝对的。HttpReponse
实例代表实际的HTTP响应。java.lang.Boolean
对象代表指示实际的请求是否被完全提交到连接目标的标志。RequestConfig
对象代表实际请求配置。java.util.List<URI>
对象代表在请求执行过程中所有重定向位置的集合。可以使用HttpClientContent
适配器类来简化与上下文状态的交互。
连续的多个请求代表逻辑相关的会话应该使用相同的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上下文存储一个请求或者多个连续请求的过程状态。
通常只有不依赖执行上下文的特定状态,那么拦截器的执行顺序就不重要。如果协议拦截器相互依赖,因此必须以特定的顺序执行,那么他们应该按照他们所期望的执行的顺序添加到协议处理器中。
协议拦截器必须实现线程安全。类似于Servlet,协议拦截器不应该使用实例变量,除非访问这些变量是同步的(synchronized)。
这是一个本地上下文如何用于在连续请求中持久化一个过程状态的示例:
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
,例如socket超时或者重置,还有表示HTTP失败的HttpException
,例如违反HTTP协议。通常I/O错误被认为是非致命的和可修复的,因此HTTP协议错误被认为是致命的并不能自动修复。请注意HttpClient
实现将HttpException
重新抛出为ClientProtocolException
,它是java.io.IOException
子类。这使HttpClient
用户从一个catch语句中同时处理I/O错误和协议违规。
理解HTTP协议不是适用于所有类型的应用程序是非常重要的。HTTP是一个简单面向request/response的协议,其最初设计为支持静态或者动态生成的内容检索。它从来没有打算支持事务操作。例如,如果服务器成功地接收并处理请求,生成响应并发送状态码到客户端,那么它则认为自己的契约部分已经完成。如果客户端由于读超时,请求取消或者系统崩溃导致无法完全接收响应,服务器不会尝试回滚事务。如果客户端决定重试相同的请求,服务器将不可避免地执行多次相同的事务。在某些情况下,将导致应用程序数据损坏或者应用程序状态不一致。
尽管HTTP从没设计支持事务处理,只要满足某些条件,它仍可以被用作关键任务应用程序的传输协议。为确保HTTP传输层安全,系统必须确保在应用程序层HTTP方法的幂等性。
HTTP/1.1将幂等方法定义为[方法也可以具有幂等的性质(除了错误和过期问题),N > 0的相同的请求与单个请求的副作用相同]。换句话说,应用程序应当确保它已经准备好处理相同方法的多次请求的影响。例如,这可以通过提供一个唯一的事务id和其他防止相同逻辑操作执行方式实现。
请注意这个问题并不是特定于HttpClient。基于浏览器的应用程序受到与HTTP方法非幂等完全相同问题的影响。
默认情况下,HttpClient假设只有非实体封装方法是幂等的,例如GET
和HEAD
并且实体封装方法POST
和PUT
出于兼容原因不是幂等的。
默认情况下,HttpClient尝试自动从I/O异常恢复。默认自动恢复机制仅限制在已知安全的几个异常。
HttpException
类)。为了启用一个自定义异常恢复机制,应该提供一个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-1616定义为幂等的这些请求方法视为自动重试的安全方法:GET
,HEAD
,PUT
,DELETE
,OPTIONS
,TRACE
。
在某些情况下,由于目标服务器的高负载或者在客户端发出太多的并发请求,HTTP请求执行无法在期望的时间内完成。在这些情况下可能需要提前终止请求并解除在I/O操作中阻塞的执行线程。通过HttpClient执行的HTTP请求通过调用HttpUriRequest#abort()
方法可以在执行的任何阶段中止。此方法是线程安全的并且可以从任何线程调用。当一个HTTP请求被中止,它的执行线程-尽管在当前I/O操作中阻塞-保证通过抛出InterruptedIOException
解除。
HttpClient自动化处理所有类型的重定向,除了那些明确被HTTP规范禁止的需要用户干预的重定向。参见其他在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<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();
}
建立一个连接从一个主机到另外一个主机的过程是非常负责的并且在两端点间调用多次包交换,其是非常消耗时间的。连接握手的消耗可能很大,尤其对于小的HTTP消息。如果打开的链接可以重用于执行多次请求可以实现更高的数据吞吐量。
HTTP/1.1声明默认情况下HTTP连接可以重用多次请求。兼容HTTP/1.0端点也可以使用一个显式地传输首选项的机制来保持连接存活并使用它多次请求。HTTP代理也可以在一段时间内保持空闲连接存活,以防后续的请求连接到相同目标主机。保持连接存活的能力通常称为连接持久化。HttpClient完全支持连接持久化。
HttpClient能够直接或者可能涉及多个中间连接的路由(也被称为hop)建立到目标主机的连接。HttpClient将路由连接分为普通连接(plain),隧道连接(tunneled)和分层连接(layered)。使用多个中间代理连接到目标主机的隧道链接被称为代理链接。
普通路由是通过连接到目标或者第一个也是唯一的代理建立的。隧道路由是通过链接到第一个并且通过代理链隧道连接到目标服务器建立的。没有代理的隧道不能称为隧道连接。分层路由是通过在现有连接上分层协议建立的。协议只能在通往目标的隧道或者在没有代理的直接连接上分层。
RouteInfo
接口表示有关到目标主机的最终的路由信息,包括一个或者多个中间步骤或者跳(hop)。HttpRoute
是RouteInfo
的具体实现,它不能被修改(是不可变的)。HttpTracker
是可变的RouteInfo
实现,HttpClient内部用来追踪到最终路由目标的剩余跳。在成功执行通往路由目标的下一个跳之后,HttpTracker
可以被变更。HttpRouteDirector
是一个帮助类,可以用于计算路由中的下一步。此类由HttpClient内部使用。
HttpRoutePlanner
是一个接口,表示基于执行的上下文计算到给定目标的完整路由的策略。HttpClient带有两个默认的HttpRoutePlanner
接口。SystemDefaultRoutePlanner
是基于java.net.ProxySelector
。默认情况下,它将从系统属性或者正在运行的应用程序的浏览器获取JVM的代理设置。DefaultProxyRoutePlanner
实现不会使用任何Java系统属性和任何系统或者浏览器代理设置。它总是使用相同的默认代理计算路由。
如果两个连接端点的之间传输的信息不能被未授权的第三方读取或者篡改,则HTTP连接被认为是安全的。SSL/TLS协议被最广泛技术用于确保HTTP传输安全。但是,其他加密技术也被使用。通常,HTTP传输在SSL/TLS加密连接上分层的。
HTTP连接是复杂的,有状态的,非线程对象,需要对其适当地管理才能正常工作。HTTP链接同一时间只能被一个执行线程中运行。HttpClient使用一个被称为HTTP链接管理器的特殊实体来管理HTTP连接的访问,由HttpClientConnnectionManager
接口表示。HTTP连接管理器的目的是充当一个创建新HTTP链接的工厂,管理持久化连接的生命周期并同步访问持久化连接,确保在同一时间只有一个线程可以访问连接。内部HTTP连接管理器使用ManageHttpClientConnection
工作,其充当一个真实连接的代理并管理连接状态和控制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模型最主要的缺点之一是网络socket只有当在一个I/O操作阻塞时才能对I/O事件作出反应。当一个连接被释放回管理器,它可以保持存活,然而它不能监控socket的状态并对任何I/O事件作出反应。如果连接在服务端关闭,客户端连接不能检测在连接状态中的变化(并通过关闭其端的socket作出适应的反应)。
HttClient在使用连接执行HTTP请求之前,通过测试连接是否是stale
(即因为在服务端已经关闭不在有效)来缓解问题。stale连接检查不是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
头数据不在response中存在,HttpClient假设连接无限期保持存活。但是通常许多HTTP服务器在使用中被配置为在一段时间不活跃后删除持久化连接以节约系统资源,经常不会通知客户端。这种情况下默认策略被证明太过乐观,一个可能想要提供一个自定义keep-alive策略。
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
接口来创建,初始化和连接socket。这使HttpClient用户在运行时提供应用程序特定的socket初始化代码。PlainConnectionSocketFactory
是默认的工厂,用于创建和初始化普通的(非加密的)socket。
创建一个socket的过程和将它与host连接的过程是藕合的,以便socket在连接操作中被阻塞时可以被关闭。
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
接口的扩展。分层socket工厂有能力在已存在的普通socket之上创建socket分层。socket分层主要用于通过代理创建安全socket。HttpClinet带有实现SSL/TLS分层的SSLSocketFactory
。请注意HttpClient不会使用任何自定义的加密功能。它是完全依赖于标准的Java加密(JCE)和安全Socket(JSEE)扩展。
自定义连接socket工厂可以与特定的协议方案(如HTTP或者HTTPS)结合,然后用于创建自定义的连接管理器。
ConnectionSocketFactory plainsf = <...>
LayeredConnectionSocketFactory sslsf = <...>
Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", plainsf)
.register("https", sslsf)
.build();
HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);
HttpClients.custom()
.setConnectionManager(cm)
.build();
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/TSL协议的概念有一定程度的熟悉,它的详细的说明已经超出了此文档的范围。请参考Java™ Secure Socket Extension (JSSE) Reference Guide-Java安全Socket扩展参考指南了解javax.net.ssl.SSLContext
和有关工具的详细的解释。
除了在SSL和TSL协议级别执行的信任的验证和客户端身份认证,一旦建立了链接,HttpClient可以选择行地验证目标主机名称是否匹配在服务器的X509证书中的名称。这种验证可以为服务器信任资料的真实性提供额外保障。javax.net.ssl.HostnameVerifier
接口代表一种主机名验证的策略。HttpClient带有两个javax.net.ssl.HostnameVerifier
接口。重要的是:主机名验证不应该与SSL信任验证混淆。
每个默认的HttpClient使用DefaultHostnameVerifier
实现。如果需要,可以指定一个不同的主机名验证器实现。
SSLContext sslContext = SSLContexts.createSystemDefault();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslContext,
NoopHostnameVerifier.INSTANCE);
HttpClient从版本4.4以来使用Mozilla Foundation维护的公共的后缀列表来确保在SSL证书中的通配符不会被误用,应用到具有公共顶级域的多个域。HttpClient附带有发布时检索到的列表的副本。最新版本列表可以在https://publicsuffix.org/list/找到。最好是在本地制作一份清单的副本,从它的原始位置下载这个副本每天不要超过一次。
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.load(
PublicSuffixMatcher.class.getResource("my-copy-effective_tld_names.dat"));
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
可以通过使用null
匹配器对公共后缀列表禁用验证。
DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier(null);
虽然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被设计为无状态的,面相request/response协议,对于横跨多个逻辑相关的request/response交换状态会话没有特殊规定。随着HTTP协议的普及和采用,越来越多的系统开始用于它从来没打算用于的应用程序,例如用于电子商务应用程序的传输。因此,状态管理的支持变得必要。
Netscape Communications,当时一个领先web客户端和服务器软件开发商,在他们的产品中基于一个专有规范实现了对HTTP状态管理支持。后来,Netscape尝试通过发布规范草案标准化此机制。这些努力促成了通过RFC标志轨道定义的正式规范。相当数量上的应用程序的状态管理仍很大程度上依据Netscape草案并与官方规范不兼容。所有主要web浏览器的开发者都觉得保留与这些应用程序的兼容性,极大地导致了标准遵守的碎片化。
HTTP cookie是HTTP供应商和目标服务器可以通过交换令牌或者状态信息短包来维护一个会话。Netscape工程师曾称它为“神奇的cookie”和一直沿用至今。
HttpClient使用Cookie
接口来表示一个抽象的cookie令牌。一个HTTP cookie最简单的格式仅仅是名称/值对。通常HTTP cookie也包含多个属性,例如一个对其有效的域,指定此cookie应用到原服务器上的URL子集的路径和cookie有效最长时间。
SetCookie
接口表示通过原服务器发送到HTTP代理的Set-Cookie
响应头数据以维护一个会话状态。
ClientCookie
接口继承了Cookie
接口,有额外的客户端特定的功能,例如能够按照原始服务器指定的方式检索原始的cookie属性。对于生成Cookie
头数据是非常重要的,因为一些cookie规范要求cookie
头数据应该包含某些在Set-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");
CookieSpec
接口表示cookie管理规范。Cookie管理规范应强制执行:
Set-Cookie
头数据解析规则。Cookie
头数据的格式。CookieSpec
实现:cookie策略可以在HTTP客户端设置,如果需要的话,可以在HTTP请求级别覆盖。
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策略,应该创建一个CookieSpec
接口自定义实现,创建一个CookieSpecProvider
实现来创建和初始化自定义规范实例并并向HttpClient注册工厂。一旦自定义规范已经注册,它可以与标准cookie规范一样的方式激活。
PublicSuffixMatcher publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault(); Registry<CookieSpecProvider> r = RegistryBuilder.<CookieSpecProvider>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();
HttpClient可以使用实现了CookieStore
接口持久化cookie存储任何物理表示。默认CookieStore
实现称为BasicCookieStore
是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请求执行的过程中,HttpClient将以下状态管理相关对象添加到执行上下文:
Lookup
表示实际cookie规范注册表的实例。本地上下文设置属性的值优先级高于默认值。CookieSpec
表示实际cookie规范实例。CookieOrigin
表示原始服务器的实际明细实例。CookieStore
表示实际的cookie存储实例。本地上下文设置属性的值优先级高于默认值。HttpContext
对象可以用于在请求执行之前定制HTTP状态管理上下文,或者在请求执行之后检查它的状态。也可以使用单独的执行上下文以实现每个用户(每个线程)的状态管理。在本地上下文中定义的cookie规范注册表和cookie存储优先级高于在HTTP客户端级别设置的默认cookir注册表和存储。CloseableHttpClient httpclient = <...>
Lookup<CookieSpecProvider> 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();
HttpClient提供了对HTTP标准规范定义的身份认证模式的完全支持,以及多个广泛使用的非标准身份认证模式,例如NTLM和SPNEGO。
任何用户身份认证的过程要求一组可用于建立用户身份的凭证。最简单格式的用户凭证可以仅仅是用户名/密码对。UsernamePasswordCredentials
表示一组由安全主体和使用明文的密码凭证。此实现满足HTTP标准规范定义的标准身份验证模式。
UsernamePasswordCredentials creds = new UsernamePasswordCredentials("user", "pwd");
System.out.println(creds.getUserPrincipal().getName());
System.out.println(creds.getPassword());
stdout >
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());
stdout >
DOMAIN/user
pwd
AuthScheme
接口代表一个抽象面向质询-响应(challenge-response)的认证模式。认证方案需要支持以下功能:
AuthScheme
实现:GSSAPI
协商机制(Simple and Protected GSSAPI
Negotiation Mechanism))是GSSAPI
的"伪机制",其用于协商众多可能的真实机制之一。SPNEGO最明显的用法是Microsoft的HTTP Negotiate
身份认证扩展。协商的子机制包含Active Directory支持的NTLM和Kerberos。目前HttpClient只支持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")));
stdout>
[principal: u1]
[principal: u2]
null
[principal: u3]
HttpClient依赖AuthState
类来追踪身份认证过程的状态的详细信息。HttpClient在HTTP请求的执行过程中创建AuthState
两个实例:一个目标是主机身份认证,另外一个是代理身份认证。假如目标服务器或者代理需要用户身份认证,将使用身份认证过程中使用的AuthScope
,AuthScheme
,Crednetials
填充各自的AuthScope
实例。可以检查AuthState
以找出请求身份认证的类型,一个匹配的AuthScheme
实现是否被找到以及凭证提供者是否设法为给定的身份认证范围找到用户凭证。
在HTTP请求执行的过程中,HttpClient将以下身份认证的相关对象添加到执行上下文:
Lookup
实例表示实际身份认证模式注册表。在本地上下文设置的属性值高于默认值。CredentialsProvider
实例表示实际身份认证提供者。在本地上下文设置的属性值高于默认值。AuthState
实例表示实际目标身份认证状态。在本地上下文设置的属性值高于默认值。AuthState
实例表示实际代理身份认证状态。在本地上下文设置的属性值高于默认值。AuthCache
实例表示实际身份认证数据缓存。在本地上下文设置的属性值高于默认值。HttpContext
对象可以用于定制HTTP身份认证上下文或者请求在执行之后检查它的状态。CloseableHttpClient httpclient = <...> CredentialsProvider credsProvider = <...> Lookup<AuthSchemeProvider> 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自动化缓存它已经成功认证的主机信息。请注意必须使用相同的执行上下文执行逻辑相关的请求,为了缓存的身份认证的数据从一个传播到另外一个。只要执行上下文超出范围,身份认证数据将会丢失。
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(); } }
从版本4.1开始,HttpClient提供对NTLMv1, NTLMv2和NTLM2 Session身份认证完全开箱即用的支持。仍可以继续使用外部NTLM
引擎,例如Samba项目开发的JCIFS类库,作为其Windows互操作性程序套件的一部分。
在计算开销和性能影响方面,NTLM
身份认证方案比标准Basic
和Digest
方案明显更加昂贵。这可能就是Microsoft选择使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
(简单受保护GSSAPI
协商机制)被设计当两端都不知道另一个可以使用/提供什么的时候允许对服务进行身份认证。最常用于Kerberos的身份认证。它可以包装其他机制,但是在当前的HttpClient版本在设计时完全考虑Kerberos。
SPNEGO
在HttpClient中的支持SPNEGO
身份验证方案兼容Sun Java1.5及以上版本。但是更推荐使用Java版本1.6以上,因为它支持SPNEGO
身份认证更完整。Sun JRE提供了支持的类来完成几乎所有Kerberos和SPNEGO
令牌处理。这意味着许多设置都针对GSS类。SPNegoScheme
是一个简单的类来处理token封存并读和写入正确的头数据。
最好的开始的方式是在示例中使用KerberosHttpClient.java
文件并尝试使他开始工作。有许多问题可能会发生,但是如果幸运的话它将不会出现太多问题。它也会提供一些用于调试的输出。
在Windows中,它应该默认使用登陆凭证。这个可以使用’kinit’(即,$JAVA_HOME\bin\kinit testuser@AD.EXAMPLE.NET
)覆盖,其对于测试和调试问题是非常有帮助的。移除kinit创建的缓存文件以回复到windows Kerberos缓存。
确保在krb5.conf
文件中列出domain_realms
。这是一个问题的主要来源。
- Client Web Browser使用HTTP GET请求资源
- Web服务器返回HTTP 401状态和请求头: WWW-Authenticate: Negotiate
- 客户端生成
NegTokenInit
,使用base64进行编码并使用认证请求头数据重复提交GET
:Authorization: Negotiate <base64 encoding>.
。- 服务器解码
NegTokenInit
,提取支持的MechTypes
(只有Kerberos V5支持我们的情况),确保它是我们期望的那个,然后提取MechToken
(Kerberos Token)并身份验证它。如果需要更多的处理,另外一个在WWW-Authenticate
头数据中带有更多数据的HTTP 401返回到客户端。客户端使用此信息并生成另外一个token,将其传递回Authorization
头数据的返回值直到完成。- 当客户端已经被认证,Web服务器应该返回HTTP200状态和最终的
WWW-Authenticate
头数据和页内容。
此文档假设你正在使用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
login.conf
文件以下配置是在Windows XP中针对IIS
和JBoss Negotiation
模块工作的最基本的设置。
系统属性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使用当前用户票证,系统属性javax.security.auth.useSubjectCredsOnly
必须设置为false
并且应该添加并正确设置Windows注册表键allowtgtsessionkey
以允许在Kerberos Ticket-Granting Ticket中发送会话key。
在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
从版本4.2开始,HttpClient带来一个易用的基于流式接口概念的门面API。流式门面API只暴露HttpClient最基本功能,用于不需要完全灵活的HttpClient的简单用例。例如,流式门面API使用户不必处理链接管理和资源回收。
以下是通过HC流式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();
流式门面API通常不必用户处理链接管理和资源回收。不过,在大多数情况下,这会带来在内存中必须缓冲响应消息内容为代价。高度推荐使用ResponseHandler
来处理HTTP响应过程以便防止在内存中必须缓冲内容。
Document result = Request.Get("http://somehost/content") .execute().handleResponse(new ResponseHandler<Document>() { 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); } } });
HttpClient缓存提供一个HTTP/1.1兼容的缓存层用于HttpClient,相当于浏览器缓存的Java版本。此实现遵守责任链模式,其缓存实现可以直接替代默认的非缓存的HttpClient实现。完全可以从缓存中满足的请求不会产生实际的源请求。使用条件的GET和If-Modified-Since和If-None-Match请求头自动校验过期缓存项的来源。
一般来说,HTTP/1.1缓存被设计为语义透明的。也就是,一个缓存不应该修改客户端和服务端之间请求响应交换的含义。同样地,将缓存HttpClient放到现有兼容客户端-服务器关系中应该是安全的。尽管,从HTTP协议的角度来看,缓存模块是客户端的一部分,但实现的目标是与置于透明缓存代理要求兼容。
最终,缓存的HttpClient包括支持RFC 5861(stale-if-error and stale-while-revalidate)规定的Cache-Control扩展。
当缓存HttpClient执行一个请求,它将经过以下流程:
我们相信HttpClient缓存无条件符合RFC-2616。也就是说,只要规范指示了HTTP缓存为MUST, MUST NOT, SHOULD,或者SHOULD NOT,缓冲层就会尝试满足这些要求方式进行操作。这意味着缓存模块在放入时不会产生不正确的行为。
这是一个如何设置基本缓存HttpClient简单的示例。根据配置,它将存储最大值1000个缓存对象,每一个对象有一个最大的body大小为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
实例来跨以下领域定制行为:
缓存大小:如果后端存储支持这些限制,你可以指定缓存项最大值数以及以及最大可缓存响应body大小。
公共/私有(Public/Private)缓存:默认情况下,缓存模块认为本身是一个共享(公共的)缓存,而不会缓存带有认证头数据或者使用“Cache-Control:private”标记的请求的响应。但是,如果缓存仅被一个逻辑用户使用(类似于浏览器缓存的行为),则需要关闭共享缓存设置。
Heuristic缓存:根据RFC2616,尽管在源上没有显性地设置缓存控制头数据,一个缓存可能缓存某一个缓存项。此行为默认是关闭的,如果你使用一个没有正确设置的头数据的源工作,但是你仍想要缓存响应,你可能想要打开它。你可能想要启用heuristic缓存,然后指定默认新鲜度生命周期和自上次修改资源以来的一小部分时间。可以查看HTTP/1.1 RFC的章节13.2.2和13.2.4了解更多关于heuristic缓存详情。
后端验证:缓存模块支持RFC5861的stale-while-revalidate(过期则重新验证)指令。其允许在后台进行某个缓存项重新校验。你可能想要稍微调整后台工作线程的最小值和最大值的数量的设置,以及在被回收之前他们保持空闲的最大时间。当没有足够的工作线程满足需求时,你也可以控制用于重新验证队列的大小。
缓存HttpClient默认实现将缓存项和缓存的响应体存储在应用程序JVM内存中。尽管这样提供了高可用,由于在大小上的限制或者因为缓存项是短暂的,应用程序重启后无法存活,因此它不一定适用你的应用程序。当前的发行版包括使用EhCache和分布式缓存对存储缓存项的支持,允许将缓存项溢出到磁盘或存储在外部进程中。
如果没有选项适合你的应用程序,你可以实现HttpCacheStorage接口提供你自己的存储后端,然后在构造时提供给缓存HttpClient。在这种情况下,缓存项将会使用你的方案被存储,你将重用所有关于HTTP1.1 遵从性和缓存处理的逻辑。通常来说,你应该用任何支持key/value存储(类似于Java Map接口)可以创建一个HttpCacheStorage实现,其具有应用原子更新的能力。
最后,通过一些额外的努力,完全可以建立多层缓存结果;例如,按照类似于虚拟内存的格式,L1/L2处理器缓存等模式,将内存缓存HttpClient封装在存储磁盘或者远端存储在内存的HttpClient周围。
为了能够处理非标准的,非兼容的行为,在某种场景有必要定制HTTP消息跨线传输方式,而不是使用HTTP参数就能做到的。例如,对于web爬虫程序(crawler),可能强制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
实现。按照要求使用自定义实现替换默认的请求writer和响应转换器。HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory =
new ManagedHttpClientConnectionFactory(
new DefaultHttpRequestWriterFactory(),
new DefaultHttpResponseParserFactory(
new MyLineParser(), new DefaultHttpResponseFactory()));
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(
connFactory);
CloseableHttpClient httpclient = HttpClients.custom()
.setConnectionManager(cm)
.build();
虽然HTTP规范假设会话状态信息一直通过HTTP cookie格式内嵌到HTTP消息中,因此HTTP连接总是无状态的,但这个假设在现实生活中并不总是正确的。有些情况下,当使用特定的用户身份创建HTTP连接,或者在特定的安全上下文中,所以不能与其他用户分享,只能相同的用户重用。这样有状态的HTTP连接的示例是NTLM
身份认证的链接和使用客户端证书身份认证SSL连接。
HttpClient依赖UserTokenHandler
接口来确定给定的执行上下文是否是指定的用户。如果是指定用户,处理器(handler)返回的对象token对象可以唯一识别当前用户,如果上下文不包含特性于用户的任何资源或者详细信息,则返回为null。用户token将用于确保用户专用资源将不会与其他用户共享和重用。
UserTokenHandler
接口的默认实现使用一个Principal类的实例来表示HTTP连接的状态对象,前提是它可以从给定的执行上下文获取。DefaultUserTokenHandler
将使用基于身份认证模式(例如NTLM
)的连接的用户主体或者使用客户端身份认证的SSL会话的用户主体。如果两者都无效,不会返回null token。
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,你可以调度http调用并将响应视为一个Future。这在对一个web服务进行多次调用时非常有用。使用FutureRequestExecutionService的优势是你可以使用多个线程并发调度请求,在任务上设置超时时间,或者当一个响应不在需要时取消他们。
FutureRequestExecutionService使用HttpRequestFutureTask封装请求,其继承了FutureTask。此类允许你取消任务并跟踪各种指标,例如请求持续时间。
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<Boolean> 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
除了手工调用task.get(),你也可以使用FutureCallback实例,当请求完成时获得回调。这与在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<Boolean> task = futureRequestExecutionService.execute( new HttpGet("http://www.google.com"), HttpClientContext.create(), new OkidokiHandler(), new MyCallback());
FutureRequestExecutionService通常用于进行大量的web服务调用应用程序中。为方便监控或者配置调优,FutureRequestExecutionService追踪多种指标。每个HttpRequestFutureTask提供方法来获取任务被调用,启动和结束的时间。额外地,请求和任务持续时间也是可用的。这些指标在FutureRequestExecutionService中的一个FutureRequestExecutionMetrics实例中汇聚,该实例可以通过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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。