spring boot封装HttpClient的示例代码
最近使用到了HttpClient,看了一下官方文档:HttpClientimplementationsareexpectedtobethreadsafe.Itisrecommendedthatthesameinstanceofthisclassisreusedformultiplerequestexecutions,翻译过来的意思就是:HttpClient的实现是线程安全的,可以重用相同的实例来执行多次请求。遇到这种描述的话,我们就应该想到,需要对HttpClient来进行封装了。由于是使用的springboot,所以下面来结合springboot来封装HttpClient。
一、Requestretryhandler(请求重试处理)
为了使自定义异常机制生效,需要实现HttpRequestRetryHandler接口,代码如下:
importjava.io.IOException; importjava.io.InterruptedIOException; importjava.net.UnknownHostException; importjavax.net.ssl.SSLException; importjavax.net.ssl.SSLHandshakeException; importorg.apache.http.HttpEntityEnclosingRequest; importorg.apache.http.HttpRequest; importorg.apache.http.NoHttpResponseException; importorg.apache.http.client.HttpRequestRetryHandler; importorg.apache.http.client.protocol.HttpClientContext; importorg.apache.http.conn.ConnectTimeoutException; importorg.apache.http.protocol.HttpContext; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; /** *描述:HttpClient的重试处理机制 */ @Configuration publicclassMyhttpRequestRetryHandler{ @Value("${httpclient.config.retryTime}")//此处建议采用@ConfigurationProperties(prefix="httpclient.config")方式,方便复用 privateintretryTime; @Bean publicHttpRequestRetryHandlerhttpRequestRetryHandler(){ //请求重试 finalintretryTime=this.retryTime; returnnewHttpRequestRetryHandler(){ publicbooleanretryRequest(IOExceptionexception,intexecutionCount,HttpContextcontext){ //Donotretryifovermaxretrycount,如果重试次数超过了retryTime,则不再重试请求 if(executionCount>=retryTime){ returnfalse; } //服务端断掉客户端的连接异常 if(exceptioninstanceofNoHttpResponseException){ returntrue; } //timeout超时重试 if(exceptioninstanceofInterruptedIOException){ returntrue; } //Unknownhost if(exceptioninstanceofUnknownHostException){ returnfalse; } //Connectionrefused if(exceptioninstanceofConnectTimeoutException){ returnfalse; } //SSLhandshakeexception if(exceptioninstanceofSSLException){ returnfalse; } HttpClientContextclientContext=HttpClientContext.adapt(context); HttpRequestrequest=clientContext.getRequest(); if(!(requestinstanceofHttpEntityEnclosingRequest)){ returntrue; } returnfalse; } }; } }
二、Poolingconnectionmanager(连接池管理)
PoolingHttpClientConnectionManager用来管理客户端的连接池,并且可以为多个线程的请求提供服务,代码如下:
importorg.apache.http.config.Registry; importorg.apache.http.config.RegistryBuilder; importorg.apache.http.conn.socket.ConnectionSocketFactory; importorg.apache.http.conn.socket.LayeredConnectionSocketFactory; importorg.apache.http.conn.socket.PlainConnectionSocketFactory; importorg.apache.http.conn.ssl.SSLConnectionSocketFactory; importorg.apache.http.impl.conn.PoolingHttpClientConnectionManager; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; @Configuration publicclassMyPoolingHttpClientConnectionManager{ /** *连接池最大连接数 */ @Value("${httpclient.config.connMaxTotal}") privateintconnMaxTotal=20; /** * */ @Value("${httpclient.config.maxPerRoute}") privateintmaxPerRoute=20; /** *连接存活时间,单位为s */ @Value("${httpclient.config.timeToLive}") privateinttimeToLive=60; @Bean publicPoolingHttpClientConnectionManagerpoolingClientConnectionManager(){ PoolingHttpClientConnectionManagerpoolHttpcConnManager=newPoolingHttpClientConnectionManager(60,TimeUnit.SECONDS); //最大连接数 poolHttpcConnManager.setMaxTotal(this.connMaxTotal); //路由基数 poolHttpcConnManager.setDefaultMaxPerRoute(this.maxPerRoute); returnpoolHttpcConnManager; } }
注意:当HttpClient实例不再需要并且即将超出范围时,重要的是关闭其连接管理器,以确保管理器保持活动的所有连接都被关闭,并释放由这些连接分配的系统资源
上面PoolingHttpClientConnectionManager类的构造函数如下:
publicPoolingHttpClientConnectionManager(finallongtimeToLive,finalTimeUnittunit){ this(getDefaultRegistry(),null,null,null,timeToLive,tunit); } privatestaticRegistrygetDefaultRegistry(){ returnRegistryBuilder. create() .register("http",PlainConnectionSocketFactory.getSocketFactory()) .register("https",SSLConnectionSocketFactory.getSocketFactory()) .build(); }
在PoolingHttpClientConnectionManager的配置中有两个最大连接数量,分别控制着总的最大连接数量和每个route的最大连接数量。如果没有显式设置,默认每个route只允许最多2个connection,总的connection数量不超过20。这个值对于很多并发度高的应用来说是不够的,必须根据实际的情况设置合适的值,思路和线程池的大小设置方式是类似的,如果所有的连接请求都是到同一个url,那可以把MaxPerRoute的值设置成和MaxTotal一致,这样就能更高效地复用连接
特别注意:想要复用一个connection就必须要让它占有的系统资源得到正确释放,释放方法如下:
如果是使用outputStream就要保证整个entity都被writeout,如果是inputStream,则再最后要记得调用inputStream.close()。或者使用EntityUtils.consume(entity)或EntityUtils.consumeQuietly(entity)来让entity被完全耗尽(后者不抛异常)来做这一工作。EntityUtils中有个toString方法也很方便的(调用这个方法最后也会自动把inputStreamclose掉的,但是在实际的测试过程中,会导致连接没有释放的现象),不过只有在可以确定收到的entity不是特别大的情况下才能使用。如果没有让整个entity被fullyconsumed,则该连接是不能被复用的,很快就会因为在连接池中取不到可用的连接超时或者阻塞在这里(因为该连接的状态将会一直是leased的,即正在被使用的状态)。所以如果想要复用connection,一定一定要记得把entityfullyconsume掉,只要检测到stream的eof,是会自动调用ConnectionHolder的releaseConnection方法进行处理的
三、Connectionkeepalivestrategy(保持连接策略)
HTTP规范没有指定持久连接可能和应该保持存活多久。一些HTTP服务器使用非标准的Keep-Alive标头来向客户端通信它们打算在服务器端保持连接的时间段(以秒为单位)。HttpClient可以使用这些信息。如果响应中不存在Keep-Alive头,HttpClient会假定连接可以无限期地保持活动。然而,一般使用的许多HTTP服务器都配置为在一段不活动状态之后删除持久连接,以便节省系统资源,而不会通知客户端。如果默认策略过于乐观,则可能需要提供自定义的保持活动策略,代码如下:
importorg.apache.http.HeaderElement; importorg.apache.http.HeaderElementIterator; importorg.apache.http.HttpResponse; importorg.apache.http.conn.ConnectionKeepAliveStrategy; importorg.apache.http.message.BasicHeaderElementIterator; importorg.apache.http.protocol.HTTP; importorg.apache.http.protocol.HttpContext; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; /** *描述:连接保持策略 *@authorchhliu */ @Configuration publicclassMyconnectionKeepAliveStrategy{ @Value("${httpclient.config.keepAliveTime}") privateintkeepAliveTime=30; @Bean("connectionKeepAliveStrategy") publicConnectionKeepAliveStrategyconnectionKeepAliveStrategy(){ returnnewConnectionKeepAliveStrategy(){ publiclonggetKeepAliveDuration(HttpResponseresponse,HttpContextcontext){ //Honor'keep-alive'header HeaderElementIteratorit=newBasicHeaderElementIterator( response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while(it.hasNext()){ HeaderElementhe=it.nextElement(); Stringparam=he.getName(); Stringvalue=he.getValue(); if(value!=null&¶m.equalsIgnoreCase("timeout")){ try{ returnLong.parseLong(value)*1000; }catch(NumberFormatExceptionignore){ } } } return30*1000; } }; } }
注意:长连接并不使用于所有的情况,尤其现在的系统,大都是部署在多台服务器上,且具有负载均衡的功能,如果我们在访问的时候,一直保持长连接,一旦那台服务器挂了,就会影响客户端,同时也不能充分的利用服务端的负载均衡的特性,反而短连接更有利一些,这些需要根据具体的需求来定,而不是一言概括。
四、HttpClientproxyconfiguration(代理配置)
用来配置代理,代码如下:
importorg.apache.http.HttpHost; importorg.apache.http.impl.conn.DefaultProxyRoutePlanner; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; /** *描述:HttpClient代理 *@authorchhliu */ @Configuration publicclassMyDefaultProxyRoutePlanner{ //代理的host地址 @Value("${httpclient.config.proxyhost}") privateStringproxyHost; //代理的端口号 @Value("${httpclient.config.proxyPort}") privateintproxyPort=8080; @Bean publicDefaultProxyRoutePlannerdefaultProxyRoutePlanner(){ HttpHostproxy=newHttpHost(this.proxyHost,this.proxyPort); returnnewDefaultProxyRoutePlanner(proxy); } }
HttpClient不仅支持简单的直连、复杂的路由策略以及代理。HttpRoutePlanner是基于http上下文情况下,客户端到服务器的路由计算策略,一般没有代理的话,就不用设置这个东西。这里有一个很关键的概念—Route:在HttpClient中,一个Route指运行环境机器->目标机器host的一条线路,也就是如果目标url的host是同一个,那么它们的route也是一样的
五、RequestConfig
用来设置请求的各种配置,代码如下:
importorg.apache.http.client.config.RequestConfig; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.context.annotation.Bean; importorg.springframework.context.annotation.Configuration; @Configuration publicclassMyRequestConfig{ @Value("${httpclient.config.connectTimeout}") privateintconnectTimeout=2000; @Value("${httpclient.config.connectRequestTimeout}") privateintconnectRequestTimeout=2000; @Value("${httpclient.config.socketTimeout}") privateintsocketTimeout=2000; @Bean publicRequestConfigconfig(){ returnRequestConfig.custom() .setConnectionRequestTimeout(this.connectRequestTimeout) .setConnectTimeout(this.connectTimeout) .setSocketTimeout(this.socketTimeout) .build(); } }
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,代码如下:
importorg.apache.http.client.HttpRequestRetryHandler; importorg.apache.http.client.config.RequestConfig; importorg.apache.http.conn.ConnectionKeepAliveStrategy; importorg.apache.http.impl.client.CloseableHttpClient; importorg.apache.http.impl.client.HttpClients; importorg.apache.http.impl.conn.DefaultProxyRoutePlanner; importorg.apache.http.impl.conn.PoolingHttpClientConnectionManager; importorg.springframework.beans.factory.DisposableBean; importorg.springframework.beans.factory.FactoryBean; importorg.springframework.beans.factory.InitializingBean; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.stereotype.Service; /** *描述:HttpClient客户端封装 */ @Service("httpClientManagerFactoryBen") publicclassHttpClientManagerFactoryBenimplementsFactoryBean,InitializingBean,DisposableBean{ /** *FactoryBean生成的目标对象 */ privateCloseableHttpClientclient; @Autowired privateConnectionKeepAliveStrategyconnectionKeepAliveStrategy; @Autowired privateHttpRequestRetryHandlerhttpRequestRetryHandler; @Autowired privateDefaultProxyRoutePlannerproxyRoutePlanner; @Autowired privatePoolingHttpClientConnectionManagerpoolHttpcConnManager; @Autowired privateRequestConfigconfig; //销毁上下文时,销毁HttpClient实例 @Override publicvoiddestroy()throwsException{ /* *调用httpClient.close()会先shutdownconnectionmanager,然后再释放该HttpClient所占用的所有资源, *关闭所有在使用或者空闲的connection包括底层socket。由于这里把它所使用的connectionmanager关闭了, *所以在下次还要进行http请求的时候,要重新new一个connectionmanager来build一个HttpClient, *也就是在需要关闭和新建Client的情况下,connectionmanager不能是单例的. */ if(null!=this.client){ this.client.close(); } } @Override//初始化实例 publicvoidafterPropertiesSet()throwsException{ /* *建议此处使用HttpClients.custom的方式来创建HttpClientBuilder,而不要使用HttpClientBuilder.create()方法来创建HttpClientBuilder *从官方文档可以得出,HttpClientBuilder是非线程安全的,但是HttpClients确实Immutable的,immutable对象不仅能够保证对象的状态不被改变, *而且还可以不使用锁机制就能被其他线程共享 */ this.client=HttpClients.custom().setConnectionManager(poolHttpcConnManager) .setRetryHandler(httpRequestRetryHandler) .setKeepAliveStrategy(connectionKeepAliveStrategy) .setRoutePlanner(proxyRoutePlanner) .setDefaultRequestConfig(config) .build(); } //返回实例的类型 @Override publicCloseableHttpClientgetObject()throwsException{ returnthis.client; } @Override publicClass>getObjectType(){ return(this.client==null?CloseableHttpClient.class:this.client.getClass()); } //构建的实例为单例 @Override publicbooleanisSingleton(){ returntrue; } }
七、增加配置文件
#代理的host httpclient.config.proxyhost=xxx.xx.xx.xx #代理端口 httpclient.config.proxyPort=8080 #连接超时或异常重试次数 httpclient.config.retryTime=3 #长连接保持时间,单位为s httpclient.config.keepAliveTime=30 #连接池最大连接数 httpclient.config.connMaxTotal=20 httpclient.config.maxPerRoute=20 #连接超时时间,单位ms httpclient.config.connectTimeout=2000 #请求超时时间 httpclient.config.connectRequestTimeout=2000 #sock超时时间 httpclient.config.socketTimeout=2000 #连接存活时间,单位s httpclient.config.timeToLive=60
八、测试
测试代码如下:
importjava.io.IOException; importjava.util.concurrent.ExecutorService; importjava.util.concurrent.Executors; importjavax.annotation.Resource; importorg.apache.http.Consts; importorg.apache.http.ParseException; importorg.apache.http.client.ClientProtocolException; importorg.apache.http.client.methods.CloseableHttpResponse; importorg.apache.http.client.methods.HttpGet; importorg.apache.http.impl.client.CloseableHttpClient; importorg.apache.http.util.EntityUtils; importorg.junit.Test; importorg.junit.runner.RunWith; importorg.springframework.boot.test.context.SpringBootTest; importorg.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest publicclassHttpClientManagerFactoryBenTest{ //注入HttpClient实例 @Resource(name="httpClientManagerFactoryBen") privateCloseableHttpClientclient; @Test publicvoidtest()throwsClientProtocolException,IOException,InterruptedException{ ExecutorServiceservice=Executors.newFixedThreadPool(2); for(inti=0;i<10;i++){ service.submit(newRunnable(){ @Override publicvoidrun(){ System.out.println("thecurrentthreadis:"+Thread.currentThread().getName()); HttpEntityentity=null; try{ HttpGetget=newHttpGet("https://localhost:8080/testjson"); //通过httpclient的execute提交请求,并用CloseableHttpResponse接受返回信息 CloseableHttpResponseresponse=client.execute(get); System.out.println("clientobject:"+client); entity=response.getEntity(); System.out.println("============"+EntityUtils.toString(entity,Consts.UTF_8)+"============="); EntityUtils.consumeQuietly(entity);//释放连接 }catch(ClientProtocolExceptione){ e.printStackTrace(); }catch(ParseExceptione){ e.printStackTrace(); }catch(IOExceptione){ e.printStackTrace(); }finally{ if(null!=entity){//释放连接 EntityUtils.consumeQuietly(entity); } } } }); } Thread.sleep(60000); } }
通过上面的几个步骤,就基本上完成了对HttpClient的封装,如果需要更细致的话,可以按照上面的思路,逐步完善,将HttpClient封装成HttpClientTemplate,因为CloseableHttpClient内部使用了回调机制,和JdbcTemplate,或者是RedisTemplate类似,直到可以以springbootstarter的方式提供服务。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。