1.1 Request 的执行
HttpClient最必不可少的功能就是执行HTTP的方法,执行HTTP方法会涉及到一个或者多个HTTP request/HTTP response交换,而这些过程通常会在HttpClient内部完成。使用者提交一个request的对象去执行,HttpClient会发送这个request到目标服务器并且获得一个对应的response对象,如果不成功的话则抛出一个异常。
自然而然,满足了上面描述的HttpClient接口就是HttpClient API的主要入口。
下面是一个最简单的request执行过程:
- CloseableHttpClient httpclient = HttpClients.createDefault();
- HttpGet httpget = new HttpGet("http://localhost/");
- CloseableHttpResponse response = httpclient.execute(httpget);
- try {
- <...>
- } finally {
- response.close();
- }
1.1.1 HTTP request
所有的HTTP request都有一个请求线,包含了请求的方法名称,请求的URI和HTTP协议版本。
HttpClient开箱即用的支持所有HTTP/1.1中定义的HTTP方法,包括GET,HEAD,POST,PUT,DELETE,TRACE 和 OPTIONS,这些HTTP方法类型都有一个与之对应的类:HttpGet,HttpHead,HttpPost,HttpPut,HttpDelete,HttpTrace 和 HttpOptions。
Request-URI是一个统一资源定位符,它指明了用于处理该request的资源的位置。HTTP request URIs包含了一个协议类型,主机名,可选端口,资源路径,可选查询参数,可选片段。
- HttpGet httpget = new HttpGet(
- "http://www.google.com/search?hl=en&q=httpclient&btnG=Google+Search&aq=f&oq=");
- HttpClient提供URIBuilder实用类用于简化request 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=
1.1.2 HTTP response
HTTP response是服务器在接收并且处理完request消息之后返回给客户端的消息,消息的第一行包含了协议版本,status code和与之关联的文本。
- 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
1.1.3 消息头处理
HTTP消息包含多个header,这些header描述了消息的属性比如content length,content type等等,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"
该接口同时也提供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
1.1.4 HTTP entity
HTTP消息可以携带跟request和response相关联的content entity。由于这些Entity是可选的,所以他们在某些request和response中可以查找到,有些则查不到。使用了entity的request被称为entity封装请求,HTTP定义了两种entity封装请求方法:POST 和 PUT。Response通常会封装一个content entity。不同的方法会有对应的异常,比如对于HEAD来说就有204 No Content,304 Not Modified, 205 Reset Content异常。
HttpClient根据内容来源将Entities区分为3种,根据内容来源区分为:
streamed: 内容来自于流或者实时产生。特别的是,这个分类包含了从HTTP responses接收到的entities,Streamed entities通常不可重复。
self-contained: 内容来自于内存或者通过独立于connection或者其他entity的手段获得,self-contained entities通常可以重复,其也是封装了entity的HTTP request最常用的Entities类型。
wrapping : 内容来自于其他的entity。
这种区分对于HTTP response输出内容时的连接管理是非常重要的。对于request entites来说,由于其是被应用创建并且通过HttpClient发送,streamed和self-contained有什么不同就没有那么重要。
由此,建议将不可重复的entites视为streamed,可以重复的entities视为self-contained。
1.1.4.1 可重复的entities
一个entity能够重复,表明它的内容可以被读取多次,这种情况只会在self contained entities中发生(比如ByteArrayEntity 或者 StringEntity)
1.1.4.2 使用HTTP entities
Entity可以同时代表二进制和文字内容,支持对字符进行编码,比如对character content进行编码。
Entity产生于request(封装内容 )执行时,或者request成功请求并且response body用于发送结果数据到客户端时。
从Entity中读取内容,可以通过HttpEntity#getContent()获取input stream,也可以向HttpEntity#writeTo(OutputStream)提供一个output stream,该方法会将内容一次性全部写回给指派的stream。
当消息到达并且Entity已经被接收时,可以用 HttpEntity#getContentType() 和 HttpEntity#getContentLength() 方法读取常用的元数据,如Content-Type 和 Content-Length 头信息,Content-Type头信息包含文本的mime-type编码信息如 text/plain 或者 text/html,该信息可以通过 HttpEntity#getContentType() 获取到,如果Header里面不包含这些信息,那么 HttpEntity#getContentLength() 会返回-1 并且 HttpEntity#getContentType() 返回 NULL,如果Header包含这些信息,那么 HttpEntity#getContentType() 会返回 Header 对象。
当创建一个出站消息的entity时,必须同时指定其相关的参数。
- 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
1.1.5 确保释放低级资源
为了保证恰当的释放系统资源,使用完毕后必须关闭entity相关的stream和response。
- 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();
- }
关闭内容流和关闭response的不同之处在于,关闭内容流会尝试通过消费entity的content来保持底层连接,而关闭response则是直接关闭和丢弃掉这个连接。
注意 HttpEntity#writeTo(OutputStream) 方法也需要在entity被写出到流之后恰当的释放系统资源。如果通过HttpEntity#getContent()获取了java.io.InputStream流的实例,那么在finally阶段也需要关闭该流。
当处理streaming entites时,可以使用 EntityUtils#consume(HttpEntity) 方法来确保entity内容已经被完全消费并且底层stream已经被关闭。
有另一种情况是,当我们只需要从response content里面获取一小部分数据,但是消费剩余数据和保持连接复用的性能损失又太高时,我们可以直接关闭response。
- 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();
- }
这样这个连接不会被复用,连接的所有资源都会被正确的释放掉。
1.1.6 消费entity内容
消费Entity内容的推荐方式是使用 HttpEntity#getContent() 或者 HttpEntity#writeTo(OuptputStream) 方法,也可以使用EntityUtils类,该类提供了一些静态方法来简化读取Entity内容或者其他信息。通过使用该类的某些方法,你可以以String /byte[] 方式来获取整个content body,从而替代直接读取 InputStream 的方式。但是,除非你知道response entity来自于可信HTTP server 并且其长度有限,否则不推荐使用EntityUtils。
- 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();
- }
某些情况下你可能读取entity content不止一次,这时就需要将内容通过内存或者磁盘的方式缓存起来,最简单的实现方式就是使用BufferedHttpEntity包装源Entity,该类会将源 Entity 的内容读取到内存中。如果通过其他方式来实现,那么就必须要保存一个源entity了。
- CloseableHttpResponse response = <...>
- HttpEntity entity = response.getEntity();
- if (entity != null) {
- entity = new BufferedHttpEntity(entity);
- }
1.1.7 生产 entity content
HttpClient提供多个可以通过HTTP连接高效的输出内容的类,这些类的实例可以跟POST和PUT等request相关联并且为request封装entity内容,HttpClient提供了最常用的数据容器如string, byte array , input stream 和文件 : 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的实现(self-contained)来替代普通的InputStreamEntity,FileEntity是一个不错的起始点。
1.1.7.1 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 encoding来编码参数并且产生出如下内容:
- param1=value1¶m2=value2
1.1.7.2 内容分块
通常来说,建议让HttpClient选择最合适的传输编码,HttpClient选择的传输编码会基于被传输的HTTP消息的属性而定。你可以通过设置HttpEntity#setChunked()为true来通知HttpClient需要进行chunk编码。注意HttpClient只是把这一标识当做一个提示使用,该值在不支持chunk编码的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);
1.1.8 Response handlers
最简单和方便处理response的方式是使用ResponseHandler,该类包含了handleResponse(HttpResponse response)方法。该方法使得用户完全不需要去操心连接管理的事情。当使用ResponseHandler时,HttpClient会自动确保将连接释放回连接管理器,无论request是否执行成功。
- 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);