源地址:http://hc.apache.org/httpcomponents-client-ga/tutorial/html/fundamentals.html
HttpClient的最重要的功能是执行HTTP方法。一个HTTP方法的执行涉及到一个或多个HTTP请求/ HTTP响应的交互,通常由HttpClient在内部处理。用户预计提供一个请求对象去执行,HttpClient发送这个预期的请求到目标服务器,并且服务器会返回一个相应的响应对象,如果执行不成功,则抛出一个异常。
HttpClient API的主要切入点是HttpClient的接口,很自然地定义了上述合约。
下面是一个关于请求执行过程的最简单形式的例子:
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://localhost/"); CloseableHttpResponse response = httpclient.execute(httpget); try { <...> } finally { response.close(); }
所有的请求都有一个请求行,由方法名,请求URI和HTTP协议版本组成。
HttpClient支持开箱即用所有在HTTP/1.1规范中定义的HTTP方法:GET,HEAD,POST,PUT,DELETE,TRACE和OPTIONS,下面是与之相对应的特定的类:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace和HttpOptions。
Request-URI是一个统一资源标识符,用来识别申请请求上的资源。HTTP请求URIs包括协议计划,主机名,可选的端口,资源路径,可选的查询和可选的碎片。
HttpGet httpget = new HttpGet( "http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");HttpClient提供了一个使用的URIBuilder类,用来简化创造和修改请求URIs等操作。
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响应是服务器接收并解释一个请求消息后返回给客户端的消息。这个消息的第一行包括协议版本,随后是一个数值类型的状态码和与之相关联的本文的短语。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); System.out.println(response.getProtocolVersion()); System.out.println(response.getStatusLine().getStatusCode()); System.out.println(response.getStatusLine().getReasonPhrase()); System.out.println(response.getStatusLine().toString());标准输出 >
HTTP/1.1 200 OK HTTP/1.1 200 OK
一个HTTP消息可以包含若干个头以及此消息的描述属性,例如内容长度,内容类型等等。HttpClient提供了一些方法用来检索,添加,移除和列举消息头。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost"); response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\""); Header h1 = response.getFirstHeader("Set-Cookie"); System.out.println(h1); Header h2 = response.getLastHeader("Set-Cookie"); System.out.println(h2); Header[] hs = response.getHeaders("Set-Cookie"); System.out.println(hs.length);标准输出 >
Set-Cookie: c1=a; path=/; domain=localhost Set-Cookie: c2=b; path="/", c3=c; domain="localhost" 2使用HeaderIterator接口获取所有 给定类型的头是最有效的办法。
HttpResponse response = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "OK"); response.addHeader("Set-Cookie", "c1=a; path=/; domain=localhost"); response.addHeader("Set-Cookie", "c2=b; path=\"/\", c3=c; domain=\"localhost\""); HeaderIterator it = response.headerIterator("Set-Cookie"); while (it.hasNext()) { System.out.println(it.next()); }标准输出 >
Set-Cookie: c1=a; path=/; domain=localhost Set-Cookie: c2=b; path="/", c3=c; domain="localhost"它也提供了便利的方法将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]); } }标准输出 >
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)。
由于一个实体可以代表二进制内容和字符内容,所以它也支持字符编码(支持后者,即字符内容)。
当执行一个附着内容的请求,或者当请求成功并且响应体被用来做结果返回到客户端时实体被创建。
从实体中读取内容,一种办法是通过HttpEntiry#getContent()方法检索输入流,它将返回一个java.io.InputStream,另一种办法是提供一个输出流给HttpEntity#writeTo(OutputStream)方法,它将一次性把所有的内容写到提供的流里并返回。
当实体接收到一个输入消息后,HttpEnttity#getContentType()和HttpEntity#getContentLength()两个方法可以用来读取常用的元数据,例如Content-Type和Content-Length等头信息(假如它们是有效的)。由于Content-Type包含像text/plain或者text/html这样的文本mime-types的字符编码,所以通常用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);标准输出 >
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(); }关闭内容流和关闭响应的区别在于,在后者立即关闭并丢弃连接时,前者会试图通过消耗实体内容保持底层连接。
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数组来代替从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(); }在某些情况下,我们可能 需要必须可以重复读取实体内容。假如这样,就必须要已某种方式缓存实体内容,可以是内存,也可以是硬盘。最简单的实现方法是通过BufferHttpEntity类来包装原始实体。这就是原始实体的内容被读到内存缓冲区的原因。 除此之外,所有的实体包装都将是原来类型的一种。( In all other ways the entity wrapper will be have the original one.)
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类来代替使用ImputStreamEntity模型。FileEntity可以作为一个好的出发点。
许多程序都需要模仿提交HTML表单的过程,例如,为了登录一个web程序或者提交输入的数据。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 encoding来编码参数并且产生如下内容:
param1=value1¶m2=value2
通常我们推荐让HttpClient选择最适合的传输编码,它基于被转移的HTTP消息的属性。不管怎样,通过设置HttpEntity#setChunked()为true,就可以通知HttpClient块编码是优先的。但要注意,HttpClient仅仅将这个标志做为一个提示,当使用不支持块编码的HTTP协议版本时,这个值将被忽略,例如HTTP/1.0。
StringEntity entity = new StringEntity("important message", ContentType.create("plain/text", Consts.UTF_8)); entity.setChunked(true); HttpPost httppost = new HttpPost("http://localhost/acrtion.do"); httppost.setEntity(entity);
使用包含handleResponse(HttpResponse response)方法的ResponseHandler接口处理响应是最简单并且最方便的方式。这个方法完全解除了用户关于连接管理的忧虑。当使用ResponseHandler时,不管是请求执行成功还是导致异常,HttpClient都会自动关注并确保释放连接到连接管理器。
CloseableHttpClient httpclient = HttpClients.createDefault(); HttpGet httpget = new HttpGet("http://localhost/json"); ResponseHandler<MyJsonObject> rh = new ResponseHandler<MyJsonObject>() { @Override public JsonObject handleResponse( final HttpResponse response) throws IOException { StatusLine statusLine = response.getStatusLine(); HttpEntity entity = response.getEntity(); if (statusLine.getStatusCode() >= 300) { throw new HttpResponseException( statusLine.getStatusCode(), statusLine.getReasonPhrase()); } if (entity == null) { throw new ClientProtocolException("Response contains no content"); } Gson gson = new GsonBuilder().create(); ContentType contentType = ContentType.getOrDefault(entity); Charset charset = contentType.getCharset(); Reader reader = new InputStreamReader(entity.getContent(), charset); return gson.fromJson(reader, MyJsonObject.class); } }; MyJsonObject myjson = client.execute(httpget, rh);
HttpClient接口代表了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的实现期望是线程安全的。推荐同一个实例被多个的请求执行重复使用。
当一个CloseableHttpClient实例不再需要使用和即将超出与之相关联的连接管理器的范围时,必须调用CloseableHttpClient#close()方法来关闭它。
CloseableHttpClient httpclient = HttpClients.createDefault(); try { <...> } finally { httpclient.close(); }
最初HTTP被设计为无状态的,面向响应-请求的协议。然而,真实世界的应用程序经常需要能够通过一些逻辑相关的请求-响应交互保存状态信息。为了是程序能够维持处理状态,HttpClient允许HTTP请求被一个特定的执行上下文执行,即HTTP上下文。如果相同的上下文被连续的请求重复使用,多个逻辑相关请求可以参与一个逻辑会话。HTTP上下文类似于java.util.Map<String, Object>,它仅仅是一个包含任意命名值的集合。一个应用程序可以在请求执行之前填充上下文属性,或者在请求执行完毕后检查上下文。
HttpContext可以包含任何对象,因此它在多线程之间共享是不安全的。建议每个线程的执行维护自己的上下文。
在HTTP请求执行的过程中,HttpClient添加了以下属性执行上下文:
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(); }
HttpClient能抛出两种类型的异常:java.io.IOException(如果发生I/O故障,例如socket超时和重置)和HttpException(表示HTTP故障,例如违反HTTP协议)。通常I/O错误被认为是非致命错误和可恢复的,HTTP协议错误则反之。
能够理解HTTP协议并不适合所有类型的应用是很重要的。HTTP仅仅是一个面向请求/响应的协议,最初设计用来支持静态或者动态生成的内容检索,它从未打算过支持事物操作。例如,当HTTP服务器成功的接收并处理请求,它将考虑执行生成一个响应并且发送一个状态码返回给客户端这个合约。即使客户端由于读取超时还未能全部接收响应,请求取消或者系统崩溃,服务器也不会试图回滚事务。如果客户端决定重复相同的请求,服务器最终不可避免地会再次执行相同的事务。在某些情况下,这将会导致应用数据腐败(corruption)或者应用状态不一致。
虽然HTTP从未被设计支持事务处理,但它仍然可以用作传输协议为满足关键任务应用提供某些条件。为了保证HTTP传输层的安全,系统必须保证应用层的HTTP方法的幂等性(idempotency)。
HTTP/1.1规范对等幂方法的定义是
[Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request]
换句话说,应用应该保证自己准备好处理多次执行相同方法的影响。这是可以实现的,例如,提供一个唯一事务ID和避免执行相同逻辑操作的其它手段。
请注意这不是HttpClient特有的问题。基于浏览器的应用都存在相同的关于HTTP方法不等幂的问题。
HttpClient假定GET和HEAD等非实体封闭方法是等幂的,而POST和PUT等实体封闭方法是不等幂的。
HttpClient默认会试图从I/O异常自动恢复。这个默认恢复机制仅限于那些已知是安全的异常。
为了保证自定义异常恢复机制的正常使用,需要提供一个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();
在某些情况下,由于目标服务器的高负载或者客户端提交很多并发请求,HTTP请求在预期的时间框架内未能执行完全。针对这种情况,必须尽早终止请求并且解除在I/O操作中阻塞的执行线程。HttpClient执行的HTTP请求可以通过调用HttpUriRequest#abort()方法在任何执行阶段终止。这个方法是线程安全的并且可以从任何线程调用。当一个HTTP请求在它的执行线程中被终止,即使现在阻塞在I/O操作中,也会抛出一个InterruptedIOException保证解除占用。
HTTP协议拦截器是一个实现了HTTP协议特定方面的程序。通常协议拦截器预计存在于包含一个特定的头或者一些相关头的组的输入信息和输入信息中。协议拦截器还能把信息附加到内容实体上 - 透明内容压缩/解压就是一个很好的例子。在封装实体类被用来修饰原始实体的地方,通常是使用装饰者模式来完成的。若干个协议拦截器可以结合在一起组成一个逻辑单元。
协议拦截器可以通过共享信息合作 - 例如一个处理状态 - 通过HTTP执行上下文。协议拦截器可以使用HTTP上下文储存一个请求或者若干个连续请求的处理状态。
通常,某个拦截器执行的顺序是无关紧要的,只要它们不依赖执行上下文的特定状态。如果一些拦截器相互依赖并且因此而必须按特定的顺序执行,那么它们就需要按照希望执行的相同的顺序添加到协议处理器中。
协议拦截器的实现必须是线程安全的。类似于servlets,除非访问的那些变量是同步的,否则它不能够使用实例变量。
这里有一个关于本地上下文如何在连续的请求之间保存处理状态的例子:
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(); } }
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<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(); }