1.1 执行请求
HttpClient最基本的功能是执行HTTP方法。一个HTTP方法的执行涉及到一个或多个HTTP请求和响应的交换,这通常是在HttpClient内部处理的。用户需要提供一个请求对象,HttpClient负责传输这个请求到目标服务器并返回相对应的响应的对象,如果执行不成功,则抛出异常。
HttpClient API的入口就是HttpClient
接口。
以下是最简单的形式的执行请求的例子:
public void chapter1_1() throws IOException {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
// handle response
} finally {
response.close();
}
}
1.1.1 HTTP请求
所有的HTTP请求都有一个请求头,其中包含方法名、请求URI和HTTP协议版本号。
HttpClient
支持所有HTTP/1.1
规格中定义的HTTP方法,包括:GET
,HEAD
, POST
, PUT
, DELETE
, TRACE
和 OPTIONS
。每种方法都有与之对应的类:HttpGet
, HttpHead
, HttpPost
, HttpPut
, HttpDelete
, HttpTrace
和 HttpOptions
。
请求URI是请求资源的统一资源描述符(Uniform Resource Identifier)。HTTP请求URI包含协议(protocol schema)、主机名(host name)、可选的端口(optional port)、资源路径(resource path)、可选的查询(optional query)和可选的分块(optional fragment)。
HttpGet httpGet = new HttpGet(
"http://www.google.com/search?h1=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("h1", "en")
.setParameter("q", "httpclient")
.setParameter("btnG", "Google+Search")
.setParameter("aq", "f")
.setParameter("oq", "")
.build();
HttpGet httpGet1 = new HttpGet(uri);
System.out.println(httpGet1.getURI());
1.1.2 HTTP响应
HTTP响应是服务器接收和处理完请求报文后所返回的报文。报文的第一行由协议版本、状态码和及其描述组成。
public void chapter1_1_2() throws Exception {
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报文包含一些用于描述报文的头部信息,如内容长度、内容类型等等 。HttpClient
提供方法去获取、添加、移除和列举这些头部信息。
public void chapter1_1_3() {
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=\"/\"; 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="/"; domain="localhost"
2
获取所有给定类型头部信息最有效方式是使用HeaderIterator
接口。
public void chapter1_1_3_2() {
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=\"/\"; domain=\"localhost\"");
BasicHeaderElementIterator 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 (NameValuePair param : params) {
System.out.println(" " + param);
}
}
}
输出>
c1 = a
path=/
domain=localhost
c2 = b
path=/
domain=localhost
1.1.4 HTTP实体
HTTP报文可以携带与请求或响应相关联的内容实体。因为实体是可选的,并不是所有的请求和响应中都包含实体。使用实体的请求被称为包含实体的请求(entity enclosing request)
。HTTP规格中定义了2种包含实体的请求的方法:POST
和 PUT
。
响应通常会包含一个内容实体。但也是例外,如HEAD
方法的响应和 204 No Content
, 304 Not Modified
, 205 Reset Content
响应。
HttpClient
区分三种实体类型,取决于它们内容的来源:
-
streamed: 内容是从流(stream)中获得,或联机生成的。这里包含从HTTP响应中的实体。
流式实体(streamed entities)
通常是不能重复的。 -
self-contained: 内容是在内存里。
自包含实体(self-contained entities)
通常是可重复的。这种类型的实体最多用于包含实体的请求(entity enclosing request)
。 - wrapping: 内容从另一实体获得。
当从HTTP响应中获取数据流时,这些区分对于连接管理来说是重要的。对于应用创建的请求实体,且仅使用HttpClient
来发送,streamed
还是 self-contained
的区别就不重要了。这种情况下,建议把不可重复的实体归为streamed
类型,可重复的为self-contained
类型。
1.1.4.1 可重复实体
可重复实体是指它的内容能够被重复读取。只有自包含实体(self-contained entities)
才是可重复的(如ByteArrayEntity
或StringEntity
)。
1.1.4.2 使用HTTP实体
因为实体可表示二进制和字符内容,所以它是支持字符编码的。
实体被创建的时机有 a) 执行包含内容的请求; b) 请求成功后,响应体使用实体将结果返回。
为了从输入报文的实体中读取内容,我们可以通过HttpEntity#getContent()
方法获取输入流java.io.InputStream
, 或者我们可以通过HttpEntity#writeTo(OutpusStream)
方法将其写到另一个给定的输出流中。
当从响应报文中接收到实体后,HttpEntity#getContentType
和HttpEntity#getContentLength
方法可以用来读取通用的元数据,如Content-Type
和Content-Length
头部信息(如果有的话)。Content-Type
头部对于文本类型的多媒体类型(如text/plain
或text/html
)来说可能包含字符编码的信息,HttpEntity#getContentEncoding()
方法可能用来读取该信息。如果头部不可用的话,HttpEntity#getContentLength
返回-1,HttpEntity#getContentType
返回NULL。如果Content-Type
可用的话,Header
对象将会被返回。
当给输出报文创建实体,元数据必须使用实体的创建者方法来创建:
public void chapter1_1_4() throws IOException {
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 确保释放底层资源
为了确保正确地的释放系统资源,我们必须关闭实体关联的内容流(stream)或者响应(response)本身。
public void chapter1_1_5() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
InputStream inputStream = entity.getContent();
try {
// do something with inputStream
} finally {
inputStream.close();
}
}
} finally {
response.close();
}
}
关闭内容流和关闭响应的区别在于,前者通过消费实体内容来试图保持底层连接,后者会立即关闭并且丢弃该连接。
请注意HttpEntity#writeTo(OUtputStream)
方法也需要确保正确释放系统资源。如果这个方法通过调用HttpEntity#getContent
方法来获取的java.io.InputStream
实例,这也需要在一个finally子句中将其关闭。
我们也可以使用EntityUtils#consume(HttpEntity)
方法来确认实体内容被完全消费并且底层流被关闭。
有一种情况,如果仅需要读取响应中的一部分内容,并且报文剩余内容的性能代价和保持连接的代价太高的话,我们可以通过关闭响应来结束内容流。
public void chapter1_1_5_1() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
InputStream inputStream = entity.getContent();
int byteOne = inputStream.read();
int byteTwo = inputStream.read();
// Do not need the rest
}
} finally {
response.close();
}
}
连接将不会被重用,而且被该连接持有的所有资源将会被正确地释放。
1.1.6 消费实体内容
消费一个实体的内容推荐的方法是使用HttpEntity#getContent()
或HttpEntity#writeTo(OutpusStream)
方法。HttpClient也包含EntityUtils
类,其他包含一些静态方法可以理容易地读取实体内容或信息。这样我们就可以使用这个类的方法来读取整个字符或字节数据内容,而不是直接操作java.io.InputStream
。然而,EntityUtils
的使用是非常不推荐的,除非响应实体是来源于一个受信的HTTP服务器并且内容的长度是有限的。
public void chapter1_1_6() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
HttpEntity entity = response.getEntity();
if (Objects.nonNull(entity)) {
long len = entity.getContentLength();
if (len != -1 && len < 2048) {
System.out.println(EntityUtils.toString(entity));
} else {
// Stream content out
// content length is too large
}
}
} finally {
response.close();
}
}
在某些情况下,我们需要重复读取实体内容。此时,实体内容必须用某种方式来缓冲,或者在内容或者在磁盘中。完成缓存的最简单方式就是把原始的实体用BufferedHttpEntity
类来包装。这能够使原来的实体被读进内存的缓存区中。
CloseableHttpResponse response = <...>
HttpEntity entity = response.getEntity();
if (entity != null) {
entity = new BufferedHttpEntity(entity);
}
1.1.7 生产实体内容
HttpClient提供一些类,这些类用来高效地将实体内容通过HTTP连接输出到流。这些类的实例能够与包含实体的请求关联, 如POST
和PUT
。HttpClient提供一些类来作为最常见的数据的容器,如字符串、字节数组、输入流和文件:StringEntity
、ByteArrayEntity
、InputStreamEntity
和FileEntity
。
public void chapter1_1_7() throws Exception {
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表单
很多应用需要去模拟提交HTML表单的过程,例如,为了登陆或提交输入数据。HttpClient提供实体类UrlEncodedFormEntity
来帮助这个提交过程。
public void chapter1_1_7_1() throws Exception {
List<NameValuePair> 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/handle.do");
httpPost.setEntity(entity);
}
UrlEncodedFormEntity
会使用URL编码来对参数进行编码,并且产生如下内容:
param1=value1¶m2=value2
1.1.7.2 内容分块(Content chunking)
通常推荐让HttpClient基于传输的HTTP报文的属性去选择使用最合适的传输编码。然而,通过设置HttpEntity#setChunked()
为true
来通知HttpClient使用chunk
编码是可能的。当然,这仅仅只是一个提示而已。如果使用不支持chunk
编码的HTTP协议,如HTTP/1.0
,该值将会被忽略。
1.1.8 响应处理器(Response handlers)
最简单并且最方便的方式去处理响应是使用ResponseHandler
接口,该接口包含handleResponse(HttpResponse response)
方法。该方法完全地把用户从连接管理中解放出来。当使用ResponseHandler
,HttpClient会自动地的确保连接会被释放回给连接管理器,不管执行请求是否成功或异常。
public void chapter1_1_8() throws Exception {
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpGet httpGet = new HttpGet("http://www.baidu.com");
ResponseHandler<MyJsonObject> rh = response -> {
StatusLine statusLine = response.getStatusLine();
HttpEntity entity = response.getEntity();
if (statusLine.getStatusCode() >= 300) {
throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
}
if (Objects.isNull(entity)) {
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 myJsonObject = httpClient.execute(httpGet, rh);
}
static class MyJsonObject {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
网友评论