一、开始使用httpclient
本文档需要使用的依赖有如下几个:
<dependency><groupId>org.apache.httpcomponentsgroupId><artifactId>httpclientartifactId><version>4.5.10version>dependency><dependency><groupId>org.apache.httpcomponentsgroupId><artifactId>httpmimeartifactId><version>4.5.10version>dependency><dependency><groupId>org.apache.httpcomponentsgroupId><artifactId>httpcoreartifactId><version>4.4.11version>dependency>
1、创建HttpClient对象。
创建httpclient对象的方式有两种,
第一种是使用默认配置创建:
CloseableHttpClient client = HttpClients.createDefault();
第二种是使用HttpClients.custom()定制:
HttpClients.custom().setDefaultRequestConfig(RequestConfig);
这其中RequestConfig的设计方式可以学习一下,也即是建造者模式。RequestConfig只有一个默认访问修饰符的构造函,这就意味着我们在使用这个类的时候不能之间构建示例;RequestConfig有很多私有属性,这些属性没有get/set方法,所以,即便是我们拿到了RequestConfig类的实例,也不能去修改其中的属性值。那么RequestConfig是如何构造实例并且为属性赋值的呢?RequestConfig在实例化的时候会利用其公共内部类Builder。使用方式为RequestConfig.custom().setXXX().builder()。调用custom方法返回一个Builder类实例,改类里有设置RequestConfig同名属性的各种方法,设置完毕后调用builder方法构造一个新的RequestConfig实例。从这个过程中可以看出来,RequestConfig对象的属性是不可变的。
有几个参数我们自己必须设置一下:
(1)connectionRequestTimeout:从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出:ConnectionPoolTimeoutException: Timeout waiting for connection from pool
(2)connectTimeout:连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
(3)socketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout
以上3个超时相关的参数如果未配置,默认为-1,意味着无限大,就是一直阻塞等待。
2、创建请求方法的实例
创建请求方法的实例并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象:
HttpGet httpGet = new HttpGet("https://www.baidu.com");
3、组装请求参数
对于get请求,可以直接在构建HttpGet对象的时候在url中加入,也可以使用URIBuilder构建参数,具体可以参考下面的两种方式:
URIBuilder builder = new URIBuilder("http://example.com/");builder.setParameter("var1", "value1").setParameter("var2", "value2");HttpGet request = new HttpGet(builder.build());
或使用了建造者模式的URIBuilder
URI uri = new URIBuilder() .setScheme("http") .setHost("www.example.cn") .setPath("/search") .setParameter("var1", "value1") .build();
对于HttpPost对象而言,可调用setEntity(HttpEntity entity)方法来设置请求参数:
//拼接参数Listparams= new ArrayList();params.add(new BasicNameValuePair("username", "vip"));params.add(new BasicNameValuePair("password", "secret"));httpPost.setEntity(new UrlEncodedFormEntity(params));
其中setEntity的参数种类可以有很多种,比如:
JSONObject params = new JSONObject();params.put("var1", "value1");params.put("var2", "value2");httpPost.setEntity(new StringEntity(params.toJSONString(), "utf-8"));
或者使用mutltipar形式传输参数:InputStream in = httpResponse.getEntity().getContent();
InputStreamBody inputStreamBody = new InputStreamBody(in, "filename");HttpEntity entity = MultipartEntityBuilder.create() .addPart("image", inputStreamBody) .addPart("sign", new StringBody("", ContentType.APPLICATION_XML)) .build();
使用MultipartEntityBuilder需要依赖httpmime包。其中的inputStreamBody是从http查询返回中获得的一个图片的输入流。
4、发送请求
调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse:
CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
5、获取返回内容
调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容。程序可通过该对象获取服务器的响应内容。
// 获取响应状态码int statusCode = httpResponse.getStatusLine().getStatusCode();// 获取返回内容HttpEntity httpEntity = httpResponse.getEntity();String content = EntityUtils.toString(httpEntity);
6、释放连接。
无论执行方法是否成功,都必须释放连接
httpClient.close();
二、http连接池
1、 为什么使用连接池
降低延迟:如果不采用连接池,每次连接发起Http请求的时候都会重新建立TCP连接(经历3次握手),用完就会关闭连接(4次挥手),如果采用连接池则减少了这部分时间损耗
支持更大的并发:如果不采用连接池,每次连接都会打开一个端口,在大并发的情况下系统的端口资源很快就会被用完,导致无法建立新的连接
2、 创建连接池
PoolingHttpClientConnectionManager cm = null;LayeredConnectionSocketFactory sslsf = null;try { sslsf = new SSLConnectionSocketFactory(SSLContext.getDefault());} catch (NoSuchAlgorithmException e) { e.printStackTrace();}Registry socketFactoryRegistry = RegistryBuilder.create() .register("https", sslsf) .register("http", new PlainConnectionSocketFactory()) .build();cm = new PoolingHttpClientConnectionManager(socketFactoryRegistry);cm.setMaxTotal(200);cm.setDefaultMaxPerRoute(20);CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(cm) .build();
三、注意事项
1、 错误关闭连接
连接池中连接都是在发起请求的时候建立,并且都是长连接。Client.java[附录]中的in.close()作用就是将用完的连接释放,下次请求可以复用,这里特别注意的是,如果不使用in.close()而仅仅使用response.close(),结果就是连接会被关闭,并且不能被复用,这样就失去了采用连接池的意义。连接池释放连接的时候,并不会直接对TCP连接的状态有任何改变,只是维护了两个Set,leased和avaliabled,leased代表被占用的连接集合,avaliabled代表可用的连接的集合,释放连接的时候仅仅是将连接从leased中remove掉了,并把连接放到avaliabled集合中。
2、 未关闭连接
try{HttpClient httpClient = HttpClientBuilder.create().build(); HttpPost request = new HttpPost(httpUrl); StringEntity params = new StringEntity(new ObjectMapper().writeValueAsString(body), "UTF-8"); request.addHeader("content-type", "application/json"); request.setEntity(params); httpClient.execute(request); //问题代码} catch (Exception ex) { logger.error("Http post error. body={}", body, ex);}
上段代码中,httpClient.execute()执行的返回值response没有被close,导致线程一直在等待。可以改用CloseableHttpClient,并保证httpClient.execute()执行的response最终被close掉。而httpClient无需close,可以重复使用。如果使用CloseableHttpResponse,则response的close也不用显示调用。
CloseableHttpClient httpClient = HttpClientBuilder.create().build();HttpPost request = new HttpPost(httpUrl);StringEntity params = new StringEntity(new ObjectMapper().writeValueAsString(body), "UTF-8");request.addHeader("content-type", "application/json");request.setEntity(params);//httpClient.execute(request);try { CloseableHttpResponse response = httpClient.execute(request); //response.close()会被自动调用} catch (Exception ex) { logger.error("Http post execute error. body={}", body, ex);}
四、附录
1、client.java
public class Client { HttpConnectionManager connManager; public T get(String path,Class clazz){ CloseableHttpClient httpClient=connManager.getHttpClient(); HttpGet httpget = new HttpGet(path); String json=null; CloseableHttpResponse response=null; try { response = httpClient.execute(httpget); InputStream in=response.getEntity().getContent(); json=IOUtils.toString(in); in.close(); } catch (UnsupportedOperationException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { if(response!=null){ try { response.close(); } catch (IOException e) { e.printStackTrace(); } } } return JSON.parseObject(json, clazz); }}
参考:
HttpClient中文网.http://www.httpclient.cn/