spring boot封装HttpClient的示例代码
最近使用到了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接口,代码如下:
import java.io.ioexception; import java.io.interruptedioexception; import java.net.unknownhostexception; import javax.net.ssl.sslexception; import javax.net.ssl.sslhandshakeexception; import org.apache.http.httpentityenclosingrequest; import org.apache.http.httprequest; import org.apache.http.nohttpresponseexception; import org.apache.http.client.httprequestretryhandler; import org.apache.http.client.protocol.httpclientcontext; import org.apache.http.conn.connecttimeoutexception; import org.apache.http.protocol.httpcontext; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; /** * 描述:httpclient的重试处理机制 */ @configuration public class myhttprequestretryhandler { @value("${httpclient.config.retrytime}")// 此处建议采用@configurationproperties(prefix="httpclient.config")方式,方便复用 private int retrytime; @bean public httprequestretryhandler httprequestretryhandler() { // 请求重试 final int retrytime = this.retrytime; return new httprequestretryhandler() { public boolean retryrequest(ioexception exception, int executioncount, httpcontext context) { // do not retry if over max retry count,如果重试次数超过了retrytime,则不再重试请求 if (executioncount >= retrytime) { return false; } // 服务端断掉客户端的连接异常 if (exception instanceof nohttpresponseexception) { return true; } // time out 超时重试 if (exception instanceof interruptedioexception) { return true; } // unknown host if (exception instanceof unknownhostexception) { return false; } // connection refused if (exception instanceof connecttimeoutexception) { return false; } // ssl handshake exception if (exception instanceof sslexception) { return false; } httpclientcontext clientcontext = httpclientcontext.adapt(context); httprequest request = clientcontext.getrequest(); if (!(request instanceof httpentityenclosingrequest)) { return true; } return false; } }; } }
二、pooling connection manager(连接池管理)
poolinghttpclientconnectionmanager用来管理客户端的连接池,并且可以为多个线程的请求提供服务,代码如下:
import org.apache.http.config.registry; import org.apache.http.config.registrybuilder; import org.apache.http.conn.socket.connectionsocketfactory; import org.apache.http.conn.socket.layeredconnectionsocketfactory; import org.apache.http.conn.socket.plainconnectionsocketfactory; import org.apache.http.conn.ssl.sslconnectionsocketfactory; import org.apache.http.impl.conn.poolinghttpclientconnectionmanager; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; @configuration public class mypoolinghttpclientconnectionmanager { /** * 连接池最大连接数 */ @value("${httpclient.config.connmaxtotal}") private int connmaxtotal = 20; /** * */ @value("${httpclient.config.maxperroute}") private int maxperroute = 20; /** * 连接存活时间,单位为s */ @value("${httpclient.config.timetolive}") private int timetolive = 60; @bean public poolinghttpclientconnectionmanager poolingclientconnectionmanager(){ poolinghttpclientconnectionmanager poolhttpcconnmanager = new poolinghttpclientconnectionmanager(60, timeunit.seconds); // 最大连接数 poolhttpcconnmanager.setmaxtotal(this.connmaxtotal); // 路由基数 poolhttpcconnmanager.setdefaultmaxperroute(this.maxperroute); return poolhttpcconnmanager; } }
注意:当httpclient实例不再需要并且即将超出范围时,重要的是关闭其连接管理器,以确保管理器保持活动的所有连接都被关闭,并释放由这些连接分配的系统资源
上面poolinghttpclientconnectionmanager类的构造函数如下:
public poolinghttpclientconnectionmanager(final long timetolive, final timeunit tunit) { this(getdefaultregistry(), null, null ,null, timetolive, tunit); } private static registry<connectionsocketfactory> getdefaultregistry() { return registrybuilder.<connectionsocketfactory>create() .register("http", plainconnectionsocketfactory.getsocketfactory()) .register("https", sslconnectionsocketfactory.getsocketfactory()) .build(); }
在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服务器都配置为在一段不活动状态之后删除持久连接,以便节省系统资源,而不会通知客户端。如果默认策略过于乐观,则可能需要提供自定义的保持活动策略,代码如下:
import org.apache.http.headerelement; import org.apache.http.headerelementiterator; import org.apache.http.httpresponse; import org.apache.http.conn.connectionkeepalivestrategy; import org.apache.http.message.basicheaderelementiterator; import org.apache.http.protocol.http; import org.apache.http.protocol.httpcontext; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; /** * 描述:连接保持策略 * @author chhliu */ @configuration public class myconnectionkeepalivestrategy { @value("${httpclient.config.keepalivetime}") private int keepalivetime = 30; @bean("connectionkeepalivestrategy") public connectionkeepalivestrategy connectionkeepalivestrategy() { return new connectionkeepalivestrategy() { public long getkeepaliveduration(httpresponse response, httpcontext context) { // honor 'keep-alive' header headerelementiterator it = new basicheaderelementiterator( response.headeriterator(http.conn_keep_alive)); while (it.hasnext()) { headerelement he = it.nextelement(); string param = he.getname(); string value = he.getvalue(); if (value != null && param.equalsignorecase("timeout")) { try { return long.parselong(value) * 1000; } catch (numberformatexception ignore) { } } } return 30 * 1000; } }; } }
注意:长连接并不使用于所有的情况,尤其现在的系统,大都是部署在多台服务器上,且具有负载均衡的功能,如果我们在访问的时候,一直保持长连接,一旦那台服务器挂了,就会影响客户端,同时也不能充分的利用服务端的负载均衡的特性,反而短连接更有利一些,这些需要根据具体的需求来定,而不是一言概括。
四、httpclient proxy configuration(代理配置)
用来配置代理,代码如下:
import org.apache.http.httphost; import org.apache.http.impl.conn.defaultproxyrouteplanner; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; /** * 描述:httpclient代理 * @author chhliu */ @configuration public class mydefaultproxyrouteplanner { // 代理的host地址 @value("${httpclient.config.proxyhost}") private string proxyhost; // 代理的端口号 @value("${httpclient.config.proxyport}") private int proxyport = 8080; @bean public defaultproxyrouteplanner defaultproxyrouteplanner(){ httphost proxy = new httphost(this.proxyhost, this.proxyport); return new defaultproxyrouteplanner(proxy); } }
httpclient不仅支持简单的直连、复杂的路由策略以及代理。httprouteplanner是基于http上下文情况下,客户端到服务器的路由计算策略,一般没有代理的话,就不用设置这个东西。这里有一个很关键的概念—route:在httpclient中,一个route指 运行环境机器->目标机器host的一条线路,也就是如果目标url的host是同一个,那么它们的route也是一样的
五、requestconfig
用来设置请求的各种配置,代码如下:
import org.apache.http.client.config.requestconfig; import org.springframework.beans.factory.annotation.value; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; @configuration public class myrequestconfig { @value("${httpclient.config.connecttimeout}") private int connecttimeout = 2000; @value("${httpclient.config.connectrequesttimeout}") private int connectrequesttimeout = 2000; @value("${httpclient.config.sockettimeout}") private int sockettimeout = 2000; @bean public requestconfig config(){ return requestconfig.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,代码如下:
import org.apache.http.client.httprequestretryhandler; import org.apache.http.client.config.requestconfig; import org.apache.http.conn.connectionkeepalivestrategy; import org.apache.http.impl.client.closeablehttpclient; import org.apache.http.impl.client.httpclients; import org.apache.http.impl.conn.defaultproxyrouteplanner; import org.apache.http.impl.conn.poolinghttpclientconnectionmanager; import org.springframework.beans.factory.disposablebean; import org.springframework.beans.factory.factorybean; import org.springframework.beans.factory.initializingbean; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.service; /** * 描述:httpclient客户端封装 */ @service("httpclientmanagerfactoryben") public class httpclientmanagerfactoryben implements factorybean<closeablehttpclient>, initializingbean, disposablebean { /** * factorybean生成的目标对象 */ private closeablehttpclient client; @autowired private connectionkeepalivestrategy connectionkeepalivestrategy; @autowired private httprequestretryhandler httprequestretryhandler; @autowired private defaultproxyrouteplanner proxyrouteplanner; @autowired private poolinghttpclientconnectionmanager poolhttpcconnmanager; @autowired private requestconfig config; // 销毁上下文时,销毁httpclient实例 @override public void destroy() throws exception { /* * 调用httpclient.close()会先shut down connection manager,然后再释放该httpclient所占用的所有资源, * 关闭所有在使用或者空闲的connection包括底层socket。由于这里把它所使用的connection manager关闭了, * 所以在下次还要进行http请求的时候,要重新new一个connection manager来build一个httpclient, * 也就是在需要关闭和新建client的情况下,connection manager不能是单例的. */ if(null != this.client){ this.client.close(); } } @override// 初始化实例 public void afterpropertiesset() throws exception { /* * 建议此处使用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 public closeablehttpclient getobject() throws exception { return this.client; } @override public class<?> getobjecttype() { return (this.client == null ? closeablehttpclient.class : this.client.getclass()); } // 构建的实例为单例 @override public boolean issingleton() { return true; } }
七、增加配置文件
# 代理的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
八、测试
测试代码如下:
import java.io.ioexception; import java.util.concurrent.executorservice; import java.util.concurrent.executors; import javax.annotation.resource; import org.apache.http.consts; import org.apache.http.parseexception; import org.apache.http.client.clientprotocolexception; import org.apache.http.client.methods.closeablehttpresponse; import org.apache.http.client.methods.httpget; import org.apache.http.impl.client.closeablehttpclient; import org.apache.http.util.entityutils; import org.junit.test; import org.junit.runner.runwith; import org.springframework.boot.test.context.springboottest; import org.springframework.test.context.junit4.springrunner; @runwith(springrunner.class) @springboottest public class httpclientmanagerfactorybentest { // 注入httpclient实例 @resource(name = "httpclientmanagerfactoryben") private closeablehttpclient client; @test public void test() throws clientprotocolexception, ioexception, interruptedexception{ executorservice service = executors.newfixedthreadpool(2); for(int i=0; i<10; i++){ service.submit(new runnable() { @override public void run() { system.out.println("the current thread is:"+thread.currentthread().getname()); httpentity entity = null; try { httpget get = new httpget("https://localhost:8080/testjson"); // 通过httpclient的execute提交 请求 ,并用closeablehttpresponse接受返回信息 closeablehttpresponse response = client.execute(get); system.out.println("client object:"+client); entity = response.getentity(); system.out.println("============"+entityutils.tostring(entity, consts.utf_8)+"============="); entityutils.consumequietly(entity);// 释放连接 } catch (clientprotocolexception e) { e.printstacktrace(); } catch (parseexception e) { e.printstacktrace(); } catch (ioexception e) { e.printstacktrace(); } finally{ if(null != entity){// 释放连接 entityutils.consumequietly(entity); } } } }); } thread.sleep(60000); } }
通过上面的几个步骤,就基本上完成了对httpclient的封装,如果需要更细致的话,可以按照上面的思路,逐步完善,将httpclient封装成httpclienttemplate,因为closeablehttpclient内部使用了回调机制,和jdbctemplate,或者是redistemplate类似,直到可以以spring boot starter的方式提供服务。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
spring boot封装HttpClient的示例代码
-
spring boot整合CAS Client实现单点登陆验证的示例
-
Spring Boot与Kotlin 整合全文搜索引擎Elasticsearch的示例代码
-
spring boot springjpa 支持多个数据源的实例代码
-
Spring Boot中使用LDAP来统一管理用户信息的示例
-
spring boot整合mybatis+mybatis-plus的示例代码
-
Spring Boot整合Spring Security的示例代码
-
Spring Boot集成Kafka的示例代码
-
java使用spring实现读写分离的示例代码
-
spring boot 自动更新静态文件和后台代码的实例