分享

spring boot封装HttpClient

 WindySky 2017-10-25 发布于广东

最近使用到了HttpClient,看了一下官方文档:HttpClient implementations are expected to be thread safe. It is recommended that the same instance of this class is reused for multiple request executions,翻译过来的意思就是:HttpClient的实现是线程安全的,可以重用相同的实例来执行多次请求。遇到这种描述的话,我们就应该想到,需要对HttpClient来进行封装了。由于是使用的spring boot,所以下面来结合spring boot来封装HttpClient。

一、Request retry handler(请求重试处理)

为了使自定义异常机制生效,需要实现HttpRequestRetryHandler接口,代码如下:

  1. import java.io.IOException;  
  2. import java.io.InterruptedIOException;  
  3. import java.net.UnknownHostException;  
  4.   
  5. import javax.net.ssl.SSLException;  
  6. import javax.net.ssl.SSLHandshakeException;  
  7.   
  8. import org.apache.http.HttpEntityEnclosingRequest;  
  9. import org.apache.http.HttpRequest;  
  10. import org.apache.http.NoHttpResponseException;  
  11. import org.apache.http.client.HttpRequestRetryHandler;  
  12. import org.apache.http.client.protocol.HttpClientContext;  
  13. import org.apache.http.conn.ConnectTimeoutException;  
  14. import org.apache.http.protocol.HttpContext;  
  15. import org.springframework.beans.factory.annotation.Value;  
  16. import org.springframework.context.annotation.Bean;  
  17. import org.springframework.context.annotation.Configuration;  
  18.   
  19. /** 
  20.  * 描述:HttpClient的重试处理机制 
  21.  */  
  22. @Configuration  
  23. public class MyhttpRequestRetryHandler {  
  24.   
  25.     @Value("${httpclient.config.retryTime}")// 此处建议采用@ConfigurationProperties(prefix="httpclient.config")方式,方便复用  
  26.     private int retryTime;  
  27.       
  28.     @Bean  
  29.     public HttpRequestRetryHandler httpRequestRetryHandler() {  
  30.         // 请求重试  
  31.         final int retryTime = this.retryTime;  
  32.         return new HttpRequestRetryHandler() {  
  33.             public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {  
  34.                 // Do not retry if over max retry count,如果重试次数超过了retryTime,则不再重试请求  
  35.                 if (executionCount >= retryTime) {  
  36.                     return false;  
  37.                 }  
  38.                 // 服务端断掉客户端的连接异常  
  39.                 if (exception instanceof NoHttpResponseException) {  
  40.                     return true;  
  41.                 }  
  42.                 // time out 超时重试  
  43.                 if (exception instanceof InterruptedIOException) {  
  44.                     return true;  
  45.                 }  
  46.                 // Unknown host  
  47.                 if (exception instanceof UnknownHostException) {  
  48.                     return false;  
  49.                 }  
  50.                 // Connection refused  
  51.                 if (exception instanceof ConnectTimeoutException) {  
  52.                     return false;  
  53.                 }  
  54.                 // SSL handshake exception  
  55.                 if (exception instanceof SSLException) {  
  56.                     return false;  
  57.                 }  
  58.                 HttpClientContext clientContext = HttpClientContext.adapt(context);  
  59.                 HttpRequest request = clientContext.getRequest();  
  60.                 if (!(request instanceof HttpEntityEnclosingRequest)) {  
  61.                     return true;  
  62.                 }  
  63.                 return false;  
  64.             }  
  65.         };  
  66.     }  
  67. }  
二、Pooling connection manager(连接池管理)

PoolingHttpClientConnectionManager用来管理客户端的连接池,并且可以为多个线程的请求提供服务,代码如下:

  1. import org.apache.http.config.Registry;  
  2. import org.apache.http.config.RegistryBuilder;  
  3. import org.apache.http.conn.socket.ConnectionSocketFactory;  
  4. import org.apache.http.conn.socket.LayeredConnectionSocketFactory;  
  5. import org.apache.http.conn.socket.PlainConnectionSocketFactory;  
  6. import org.apache.http.conn.ssl.SSLConnectionSocketFactory;  
  7. import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;  
  8. import org.springframework.beans.factory.annotation.Value;  
  9. import org.springframework.context.annotation.Bean;  
  10. import org.springframework.context.annotation.Configuration;  
  11.   
  12. @Configuration  
  13. public class MyPoolingHttpClientConnectionManager {  
  14.     /** 
  15.      * 连接池最大连接数 
  16.      */  
  17.     @Value("${httpclient.config.connMaxTotal}")  
  18.     private int connMaxTotal = 20;  
  19.       
  20.     /** 
  21.      *  
  22.      */  
  23.     @Value("${httpclient.config.maxPerRoute}")  
  24.     private int maxPerRoute = 20;  
  25.   
  26.         /** 
  27.      * 连接存活时间,单位为s 
  28.      */  
  29.      @Value("${httpclient.config.timeToLive}")  
  30.      private int timeToLive = 60;  
  31.   
  32.        @Bean  
  33.     public PoolingHttpClientConnectionManager poolingClientConnectionManager(){  
  34.         PoolingHttpClientConnectionManager poolHttpcConnManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);  
  35.         // 最大连接数  
  36.         poolHttpcConnManager.setMaxTotal(this.connMaxTotal);  
  37.         // 路由基数  
  38.         poolHttpcConnManager.setDefaultMaxPerRoute(this.maxPerRoute);  
  39.         return poolHttpcConnManager;  
  40.     }  
  41. }  

注意:当HttpClient实例不再需要并且即将超出范围时,重要的是关闭其连接管理器,以确保管理器保持活动的所有连接都被关闭,并释放由这些连接分配的系统资源

上面PoolingHttpClientConnectionManager类的构造函数如下:

  1. public PoolingHttpClientConnectionManager(final long timeToLive, final TimeUnit tunit) {  
  2.         this(getDefaultRegistry(), null, null ,null, timeToLive, tunit);  
  3.     }  
  4.   
  5. private static Registry<ConnectionSocketFactory> getDefaultRegistry() {  
  6.         return RegistryBuilder.<ConnectionSocketFactory>create()  
  7.                 .register("http", PlainConnectionSocketFactory.getSocketFactory())  
  8.                 .register("https", SSLConnectionSocketFactory.getSocketFactory())  
  9.                 .build();  
  10.     }  

        在PoolingHttpClientConnectionManager的配置中有两个最大连接数量,分别控制着总的最大连接数量和每个route的最大连接数量。如果没有显式设置,默认每个route只允许最多2个connection,总的connection数量不超过20。这个值对于很多并发度高的应用来说是不够的,必须根据实际的情况设置合适的值,思路和线程池的大小设置方式是类似的,如果所有的连接请求都是到同一个url,那可以把MaxPerRoute的值设置成和MaxTotal一致,这样就能更高效地复用连接

特别注意:想要复用一个connection就必须要让它占有的系统资源得到正确释放,释放方法如下:

        如果是使用outputStream就要保证整个entity都被write out,如果是inputStream,则再最后要记得调用inputStream.close()。或者使用EntityUtils.consume(entity)或EntityUtils.consumeQuietly(entity)来让entity被完全耗尽(后者不抛异常)来做这一工作。EntityUtils中有个toString方法也很方便的(调用这个方法最后也会自动把inputStream close掉的,但是在实际的测试过程中,会导致连接没有释放的现象),不过只有在可以确定收到的entity不是特别大的情况下才能使用。如果没有让整个entity被fully consumed,则该连接是不能被复用的,很快就会因为在连接池中取不到可用的连接超时或者阻塞在这里(因为该连接的状态将会一直是leased的,即正在被使用的状态)。所以如果想要复用connection,一定一定要记得把entity fully consume掉,只要检测到stream的eof,是会自动调用ConnectionHolder的releaseConnection方法进行处理的

三、Connection keep alive strategy(保持连接策略)

        HTTP规范没有指定持久连接可能和应该保持存活多久。一些HTTP服务器使用非标准的Keep-Alive标头来向客户端通信它们打算在服务器端保持连接的时间段(以秒为单位)。HttpClient可以使用这些信息。如果响应中不存在Keep-Alive头,HttpClient会假定连接可以无限期地保持活动。然而,一般使用的许多HTTP服务器都配置为在一段不活动状态之后删除持久连接,以便节省系统资源,而不会通知客户端。如果默认策略过于乐观,则可能需要提供自定义的保持活动策略,代码如下:

  1. import org.apache.http.HeaderElement;  
  2. import org.apache.http.HeaderElementIterator;  
  3. import org.apache.http.HttpResponse;  
  4. import org.apache.http.conn.ConnectionKeepAliveStrategy;  
  5. import org.apache.http.message.BasicHeaderElementIterator;  
  6. import org.apache.http.protocol.HTTP;  
  7. import org.apache.http.protocol.HttpContext;  
  8. import org.springframework.beans.factory.annotation.Value;  
  9. import org.springframework.context.annotation.Bean;  
  10. import org.springframework.context.annotation.Configuration;  
  11.   
  12. /** 
  13.  * 描述:连接保持策略 
  14.  * @author chhliu 
  15.  */  
  16. @Configuration  
  17. public class MyconnectionKeepAliveStrategy {  
  18.       
  19.     @Value("${httpclient.config.keepAliveTime}")  
  20.     private int keepAliveTime = 30;  
  21.       
  22.     @Bean("connectionKeepAliveStrategy")  
  23.     public ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {  
  24.         return new ConnectionKeepAliveStrategy() {  
  25.   
  26.             public long getKeepAliveDuration(HttpResponse response, HttpContext context) {  
  27.                 // Honor 'keep-alive' header  
  28.                 HeaderElementIterator it = new BasicHeaderElementIterator(  
  29.                         response.headerIterator(HTTP.CONN_KEEP_ALIVE));  
  30.                 while (it.hasNext()) {  
  31.                     HeaderElement he = it.nextElement();  
  32.                     String param = he.getName();  
  33.                     String value = he.getValue();  
  34.                     if (value != null && param.equalsIgnoreCase("timeout")) {  
  35.                         try {  
  36.                             return Long.parseLong(value) * 1000;  
  37.                         } catch (NumberFormatException ignore) {  
  38.                         }  
  39.                     }  
  40.                 }  
  41.                 return 30 * 1000;  
  42.             }  
  43.         };  
  44.     }  
  45. }  

注意:长连接并不使用于所有的情况,尤其现在的系统,大都是部署在多台服务器上,且具有负载均衡的功能,如果我们在访问的时候,一直保持长连接,一旦那台服务器挂了,就会影响客户端,同时也不能充分的利用服务端的负载均衡的特性,反而短连接更有利一些,这些需要根据具体的需求来定,而不是一言概括。

四、HttpClient proxy configuration(代理配置)

用来配置代理,代码如下:

  1. import org.apache.http.HttpHost;  
  2. import org.apache.http.impl.conn.DefaultProxyRoutePlanner;  
  3. import org.springframework.beans.factory.annotation.Value;  
  4. import org.springframework.context.annotation.Bean;  
  5. import org.springframework.context.annotation.Configuration;  
  6.   
  7. /** 
  8.  * 描述:HttpClient代理 
  9.  * @author chhliu 
  10.  */  
  11. @Configuration  
  12. public class MyDefaultProxyRoutePlanner {  
  13.     // 代理的host地址  
  14.     @Value("${httpclient.config.proxyhost}")  
  15.     private String proxyHost;  
  16.       
  17.     // 代理的端口号  
  18.     @Value("${httpclient.config.proxyPort}")  
  19.     private int proxyPort = 8080;  
  20.       
  21.     @Bean  
  22.     public DefaultProxyRoutePlanner defaultProxyRoutePlanner(){  
  23.         HttpHost proxy = new HttpHost(this.proxyHost, this.proxyPort);  
  24.         return new DefaultProxyRoutePlanner(proxy);  
  25.     }  
  26. }  

        HttpClient不仅支持简单的直连、复杂的路由策略以及代理。HttpRoutePlanner是基于http上下文情况下,客户端到服务器的路由计算策略,一般没有代理的话,就不用设置这个东西。这里有一个很关键的概念—Route:在HttpClient中,一个Route指 运行环境机器->目标机器host的一条线路,也就是如果目标url的host是同一个,那么它们的route也是一样的

五、RequestConfig

用来设置请求的各种配置,代码如下:

  1. import org.apache.http.client.config.RequestConfig;  
  2. import org.springframework.beans.factory.annotation.Value;  
  3. import org.springframework.context.annotation.Bean;  
  4. import org.springframework.context.annotation.Configuration;  
  5.   
  6. @Configuration  
  7. public class MyRequestConfig {  
  8.     @Value("${httpclient.config.connectTimeout}")  
  9.     private int connectTimeout = 2000;  
  10.       
  11.     @Value("${httpclient.config.connectRequestTimeout}")  
  12.     private int connectRequestTimeout = 2000;  
  13.       
  14.     @Value("${httpclient.config.socketTimeout}")  
  15.     private int socketTimeout = 2000;  
  16.     @Bean  
  17.     public RequestConfig config(){  
  18.         return RequestConfig.custom()  
  19.                 .setConnectionRequestTimeout(this.connectRequestTimeout)  
  20.                 .setConnectTimeout(this.connectTimeout)  
  21.                 .setSocketTimeout(this.socketTimeout)  
  22.                 .build();  
  23.     }  
  24. }  

        RequestConfig是对request的一些配置。里面比较重要的有三个超时时间,默认的情况下这三个超时时间都为0(如果不设置request的Config,会在execute的过程中使用HttpClientParamConfig的getRequestConfig中用默认参数进行设置),这也就意味着无限等待,很容易导致所有的请求阻塞在这个地方无限期等待。这三个超时时间为:
        a、connectionRequestTimeout—从连接池中取连接的超时时间
        这个时间定义的是从ConnectionManager管理的连接池中取出连接的超时时间, 如果连接池中没有可用的连接,则request会被阻塞,最长等待connectionRequestTimeout的时间,如果还没有被服务,则抛出ConnectionPoolTimeoutException异常,不继续等待。
        b、connectTimeout—连接超时时间
        这个时间定义了通过网络与服务器建立连接的超时时间,也就是取得了连接池中的某个连接之后到接通目标url的连接等待时间。发生超时,会抛出ConnectionTimeoutException异常。
        c、socketTimeout—请求超时时间
        这个时间定义了socket读数据的超时时间,也就是连接到服务器之后到从服务器获取响应数据需要等待的时间,或者说是连接上一个url之后到获取response的返回等待时间。发生超时,会抛出SocketTimeoutException异常。

六、实例化HttpClient

通过实现FactoryBean来实例化HttpClient,代码如下:

  1. import org.apache.http.client.HttpRequestRetryHandler;  
  2. import org.apache.http.client.config.RequestConfig;  
  3. import org.apache.http.conn.ConnectionKeepAliveStrategy;  
  4. import org.apache.http.impl.client.CloseableHttpClient;  
  5. import org.apache.http.impl.client.HttpClients;  
  6. import org.apache.http.impl.conn.DefaultProxyRoutePlanner;  
  7. import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;  
  8. import org.springframework.beans.factory.DisposableBean;  
  9. import org.springframework.beans.factory.FactoryBean;  
  10. import org.springframework.beans.factory.InitializingBean;  
  11. import org.springframework.beans.factory.annotation.Autowired;  
  12. import org.springframework.stereotype.Service;  
  13.   
  14. /** 
  15.  * 描述:HttpClient客户端封装 
  16.  */  
  17. @Service("httpClientManagerFactoryBen")  
  18. public class HttpClientManagerFactoryBen implements FactoryBean<CloseableHttpClient>, InitializingBean, DisposableBean {  
  19.   
  20.     /** 
  21.      * FactoryBean生成的目标对象 
  22.      */  
  23.     private CloseableHttpClient client;  
  24.       
  25.     @Autowired  
  26.     private ConnectionKeepAliveStrategy connectionKeepAliveStrategy;  
  27.       
  28.     @Autowired  
  29.     private HttpRequestRetryHandler httpRequestRetryHandler;  
  30.       
  31.     @Autowired  
  32.     private DefaultProxyRoutePlanner proxyRoutePlanner;  
  33.       
  34.     @Autowired  
  35.     private PoolingHttpClientConnectionManager poolHttpcConnManager;  
  36.       
  37.     @Autowired  
  38.     private RequestConfig config;  
  39.       
  40.         // 销毁上下文时,销毁HttpClient实例  
  41.     @Override  
  42.     public void destroy() throws Exception {  
  43.                  /* 
  44.            * 调用httpClient.close()会先shut down connection manager,然后再释放该HttpClient所占用的所有资源, 
  45.            * 关闭所有在使用或者空闲的connection包括底层socket。由于这里把它所使用的connection manager关闭了, 
  46.            * 所以在下次还要进行http请求的时候,要重新new一个connection manager来build一个HttpClient, 
  47.            * 也就是在需要关闭和新建Client的情况下,connection manager不能是单例的. 
  48.            */  
  49.                 if(null != this.client){  
  50.             this.client.close();  
  51.            }  
  52.     }  
  53.   
  54.     @Override// 初始化实例  
  55.     public void afterPropertiesSet() throws Exception {  
  56.                  /* 
  57.          * 建议此处使用HttpClients.custom的方式来创建HttpClientBuilder,而不要使用HttpClientBuilder.create()方法来创建HttpClientBuilder 
  58.          * 从官方文档可以得出,HttpClientBuilder是非线程安全的,但是HttpClients确实Immutable的,immutable 对象不仅能够保证对象的状态不被改变, 
  59.          * 而且还可以不使用锁机制就能被其他线程共享 
  60.          */  
  61.                   this.client = HttpClients.custom().setConnectionManager(poolHttpcConnManager)  
  62.                 .setRetryHandler(httpRequestRetryHandler)  
  63.                 .setKeepAliveStrategy(connectionKeepAliveStrategy)  
  64.                 .setRoutePlanner(proxyRoutePlanner)  
  65.                 .setDefaultRequestConfig(config)  
  66.                 .build();  
  67.     }  
  68.   
  69.         // 返回实例的类型  
  70.     @Override  
  71.     public CloseableHttpClient getObject() throws Exception {  
  72.         return this.client;  
  73.     }  
  74.   
  75.     @Override  
  76.     public Class<?> getObjectType() {  
  77.         return (this.client == null ? CloseableHttpClient.class : this.client.getClass());  
  78.     }  
  79.   
  80.         // 构建的实例为单例  
  81.     @Override  
  82.     public boolean isSingleton() {  
  83.         return true;  
  84.     }  
  85.   
  86. }  
七、增加配置文件

  1. # 代理的host  
  2. httpclient.config.proxyhost=xxx.xx.xx.xx  
  3. # 代理端口  
  4. httpclient.config.proxyPort=8080  
  5. # 连接超时或异常重试次数  
  6. httpclient.config.retryTime=3  
  7. # 长连接保持时间,单位为s  
  8. httpclient.config.keepAliveTime=30  
  9. # 连接池最大连接数  
  10. httpclient.config.connMaxTotal=20  
  11. httpclient.config.maxPerRoute=20  
  12. # 连接超时时间,单位ms  
  13. httpclient.config.connectTimeout=2000  
  14. # 请求超时时间  
  15. httpclient.config.connectRequestTimeout=2000  
  16. # sock超时时间  
  17. httpclient.config.socketTimeout=2000  
  18. # 连接存活时间,单位s  
  19. httpclient.config.timeToLive=60  
八、测试

测试代码如下:

  1. import java.io.IOException;  
  2. import java.util.concurrent.ExecutorService;  
  3. import java.util.concurrent.Executors;  
  4.   
  5. import javax.annotation.Resource;  
  6.   
  7. import org.apache.http.Consts;  
  8. import org.apache.http.ParseException;  
  9. import org.apache.http.client.ClientProtocolException;  
  10. import org.apache.http.client.methods.CloseableHttpResponse;  
  11. import org.apache.http.client.methods.HttpGet;  
  12. import org.apache.http.impl.client.CloseableHttpClient;  
  13. import org.apache.http.util.EntityUtils;  
  14. import org.junit.Test;  
  15. import org.junit.runner.RunWith;  
  16. import org.springframework.boot.test.context.SpringBootTest;  
  17. import org.springframework.test.context.junit4.SpringRunner;  
  18.   
  19. @RunWith(SpringRunner.class)  
  20. @SpringBootTest  
  21. public class HttpClientManagerFactoryBenTest {  
  22.         // 注入HttpClient实例  
  23.        @Resource(name = "httpClientManagerFactoryBen")  
  24.     private CloseableHttpClient client;  
  25.       
  26.     @Test  
  27.     public void test() throws ClientProtocolException, IOException, InterruptedException{  
  28.         ExecutorService service = Executors.newFixedThreadPool(2);  
  29.         for(int i=0; i<10; i++){  
  30.             service.submit(new Runnable() {  
  31.                   
  32.                 @Override  
  33.                 public void run() {  
  34.                     System.out.println("the current thread is:"+Thread.currentThread().getName());  
  35.                                         HttpEntity entity = null;  
  36.                                        try {  
  37.                         HttpGet get = new HttpGet("https://localhost:8080/testjson");  
  38.                         // 通过httpclient的execute提交 请求 ,并用CloseableHttpResponse接受返回信息  
  39.                         CloseableHttpResponse response = client.execute(get);  
  40.                         System.out.println("client object:"+client);  
  41.                                                 entity = response.getEntity();  
  42.                                                System.out.println("============"+EntityUtils.toString(entity, Consts.UTF_8)+"=============");  
  43.                                                 EntityUtils.consumeQuietly(entity);// 释放连接  
  44.                                        } catch (ClientProtocolException e) {  
  45.                         e.printStackTrace();  
  46.                     } catch (ParseException e) {  
  47.                         e.printStackTrace();  
  48.                     } catch (IOException e) {  
  49.                         e.printStackTrace();  
  50.                     } finally{  
  51.                                                 if(null != entity){// 释放连接  
  52.                                EntityUtils.consumeQuietly(entity);  
  53.                               }  
  54.                                         }  
  55.                                 }  
  56.             });  
  57.         }  
  58.         Thread.sleep(60000);  
  59.     }  
  60. }  
通过上面的几个步骤,就基本上完成了对HttpClient的封装,如果需要更细致的话,可以按照上面的思路,逐步完善,将HttpClient封装成HttpClientTemplate,因为CloseableHttpClient内部使用了回调机制,和JdbcTemplate,或者是RedisTemplate类似,直到可以以spring boot starter的方式提供服务。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多