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的實(shí)現(xiàn)是線程安全的,可以重用相同的實(shí)例來執(zhí)行多次請(qǐng)求。遇到這種描述的話,我們就應(yīng)該想到,需要對(duì)HttpClient來進(jìn)行封裝了。由于是使用的spring boot,所以下面來結(jié)合spring boot來封裝HttpClient。
一、Request retry handler(請(qǐng)求重試處理)
為了使自定義異常機(jī)制生效,需要實(shí)現(xiàn)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的重試處理機(jī)制
*/
@Configuration
public class MyhttpRequestRetryHandler {
@Value("${httpclient.config.retryTime}")// 此處建議采用@ConfigurationProperties(prefix="httpclient.config")方式,方便復(fù)用
private int retryTime;
@Bean
public HttpRequestRetryHandler httpRequestRetryHandler() {
// 請(qǐng)求重試
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,如果重試次數(shù)超過了retryTime,則不再重試請(qǐng)求
if (executionCount >= retryTime) {
return false;
}
// 服務(wù)端斷掉客戶端的連接異常
if (exception instanceof NoHttpResponseException) {
return true;
}
// time out 超時(shí)重試
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用來管理客戶端的連接池,并且可以為多個(gè)線程的請(qǐng)求提供服務(wù),代碼如下:
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 {
/**
* 連接池最大連接數(shù)
*/
@Value("${httpclient.config.connMaxTotal}")
private int connMaxTotal = 20;
/**
*
*/
@Value("${httpclient.config.maxPerRoute}")
private int maxPerRoute = 20;
/**
* 連接存活時(shí)間,單位為s
*/
@Value("${httpclient.config.timeToLive}")
private int timeToLive = 60;
@Bean
public PoolingHttpClientConnectionManager poolingClientConnectionManager(){
PoolingHttpClientConnectionManager poolHttpcConnManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS);
// 最大連接數(shù)
poolHttpcConnManager.setMaxTotal(this.connMaxTotal);
// 路由基數(shù)
poolHttpcConnManager.setDefaultMaxPerRoute(this.maxPerRoute);
return poolHttpcConnManager;
}
}
注意:當(dāng)HttpClient實(shí)例不再需要并且即將超出范圍時(shí),重要的是關(guān)閉其連接管理器,以確保管理器保持活動(dòng)的所有連接都被關(guān)閉,并釋放由這些連接分配的系統(tǒng)資源
上面PoolingHttpClientConnectionManager類的構(gòu)造函數(shù)如下:
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的配置中有兩個(gè)最大連接數(shù)量,分別控制著總的最大連接數(shù)量和每個(gè)route的最大連接數(shù)量。如果沒有顯式設(shè)置,默認(rèn)每個(gè)route只允許最多2個(gè)connection,總的connection數(shù)量不超過20。這個(gè)值對(duì)于很多并發(fā)度高的應(yīng)用來說是不夠的,必須根據(jù)實(shí)際的情況設(shè)置合適的值,思路和線程池的大小設(shè)置方式是類似的,如果所有的連接請(qǐng)求都是到同一個(gè)url,那可以把MaxPerRoute的值設(shè)置成和MaxTotal一致,這樣就能更高效地復(fù)用連接
特別注意:想要復(fù)用一個(gè)connection就必須要讓它占有的系統(tǒng)資源得到正確釋放,釋放方法如下:
如果是使用outputStream就要保證整個(gè)entity都被write out,如果是inputStream,則再最后要記得調(diào)用inputStream.close()?;蛘呤褂肊ntityUtils.consume(entity)或EntityUtils.consumeQuietly(entity)來讓entity被完全耗盡(后者不拋異常)來做這一工作。EntityUtils中有個(gè)toString方法也很方便的(調(diào)用這個(gè)方法最后也會(huì)自動(dòng)把inputStream close掉的,但是在實(shí)際的測(cè)試過程中,會(huì)導(dǎo)致連接沒有釋放的現(xiàn)象),不過只有在可以確定收到的entity不是特別大的情況下才能使用。如果沒有讓整個(gè)entity被fully consumed,則該連接是不能被復(fù)用的,很快就會(huì)因?yàn)樵谶B接池中取不到可用的連接超時(shí)或者阻塞在這里(因?yàn)樵撨B接的狀態(tài)將會(huì)一直是leased的,即正在被使用的狀態(tài))。所以如果想要復(fù)用connection,一定一定要記得把entity fully consume掉,只要檢測(cè)到stream的eof,是會(huì)自動(dòng)調(diào)用ConnectionHolder的releaseConnection方法進(jìn)行處理的
三、Connection keep alive strategy(保持連接策略)
HTTP規(guī)范沒有指定持久連接可能和應(yīng)該保持存活多久。一些HTTP服務(wù)器使用非標(biāo)準(zhǔn)的Keep-Alive標(biāo)頭來向客戶端通信它們打算在服務(wù)器端保持連接的時(shí)間段(以秒為單位)。HttpClient可以使用這些信息。如果響應(yīng)中不存在Keep-Alive頭,HttpClient會(huì)假定連接可以無限期地保持活動(dòng)。然而,一般使用的許多HTTP服務(wù)器都配置為在一段不活動(dòng)狀態(tài)之后刪除持久連接,以便節(jié)省系統(tǒng)資源,而不會(huì)通知客戶端。如果默認(rèn)策略過于樂觀,則可能需要提供自定義的保持活動(dòng)策略,代碼如下:
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;
}
};
}
}
注意:長(zhǎng)連接并不使用于所有的情況,尤其現(xiàn)在的系統(tǒng),大都是部署在多臺(tái)服務(wù)器上,且具有負(fù)載均衡的功能,如果我們?cè)谠L問的時(shí)候,一直保持長(zhǎng)連接,一旦那臺(tái)服務(wù)器掛了,就會(huì)影響客戶端,同時(shí)也不能充分的利用服務(wù)端的負(fù)載均衡的特性,反而短連接更有利一些,這些需要根據(jù)具體的需求來定,而不是一言概括。
四、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;
// 代理的端口號(hào)
@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不僅支持簡(jiǎn)單的直連、復(fù)雜的路由策略以及代理。HttpRoutePlanner是基于http上下文情況下,客戶端到服務(wù)器的路由計(jì)算策略,一般沒有代理的話,就不用設(shè)置這個(gè)東西。這里有一個(gè)很關(guān)鍵的概念—Route:在HttpClient中,一個(gè)Route指 運(yùn)行環(huán)境機(jī)器->目標(biāo)機(jī)器host的一條線路,也就是如果目標(biāo)url的host是同一個(gè),那么它們的route也是一樣的
五、RequestConfig
用來設(shè)置請(qǐng)求的各種配置,代碼如下:
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是對(duì)request的一些配置。里面比較重要的有三個(gè)超時(shí)時(shí)間,默認(rèn)的情況下這三個(gè)超時(shí)時(shí)間都為0(如果不設(shè)置request的Config,會(huì)在execute的過程中使用HttpClientParamConfig的getRequestConfig中用默認(rèn)參數(shù)進(jìn)行設(shè)置),這也就意味著無限等待,很容易導(dǎo)致所有的請(qǐng)求阻塞在這個(gè)地方無限期等待。這三個(gè)超時(shí)時(shí)間為:
a、connectionRequestTimeout—從連接池中取連接的超時(shí)時(shí)間
這個(gè)時(shí)間定義的是從ConnectionManager管理的連接池中取出連接的超時(shí)時(shí)間, 如果連接池中沒有可用的連接,則request會(huì)被阻塞,最長(zhǎng)等待connectionRequestTimeout的時(shí)間,如果還沒有被服務(wù),則拋出ConnectionPoolTimeoutException異常,不繼續(xù)等待。
b、connectTimeout—連接超時(shí)時(shí)間
這個(gè)時(shí)間定義了通過網(wǎng)絡(luò)與服務(wù)器建立連接的超時(shí)時(shí)間,也就是取得了連接池中的某個(gè)連接之后到接通目標(biāo)url的連接等待時(shí)間。發(fā)生超時(shí),會(huì)拋出ConnectionTimeoutException異常。
c、socketTimeout—請(qǐng)求超時(shí)時(shí)間
這個(gè)時(shí)間定義了socket讀數(shù)據(jù)的超時(shí)時(shí)間,也就是連接到服務(wù)器之后到從服務(wù)器獲取響應(yīng)數(shù)據(jù)需要等待的時(shí)間,或者說是連接上一個(gè)url之后到獲取response的返回等待時(shí)間。發(fā)生超時(shí),會(huì)拋出SocketTimeoutException異常。
六、實(shí)例化HttpClient
通過實(shí)現(xiàn)FactoryBean來實(shí)例化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生成的目標(biāo)對(duì)象
*/
private CloseableHttpClient client;
@Autowired
private ConnectionKeepAliveStrategy connectionKeepAliveStrategy;
@Autowired
private HttpRequestRetryHandler httpRequestRetryHandler;
@Autowired
private DefaultProxyRoutePlanner proxyRoutePlanner;
@Autowired
private PoolingHttpClientConnectionManager poolHttpcConnManager;
@Autowired
private RequestConfig config;
// 銷毀上下文時(shí),銷毀HttpClient實(shí)例
@Override
public void destroy() throws Exception {
/*
* 調(diào)用httpClient.close()會(huì)先shut down connection manager,然后再釋放該HttpClient所占用的所有資源,
* 關(guān)閉所有在使用或者空閑的connection包括底層socket。由于這里把它所使用的connection manager關(guān)閉了,
* 所以在下次還要進(jìn)行http請(qǐng)求的時(shí)候,要重新new一個(gè)connection manager來build一個(gè)HttpClient,
* 也就是在需要關(guān)閉和新建Client的情況下,connection manager不能是單例的.
*/
if(null != this.client){
this.client.close();
}
}
@Override// 初始化實(shí)例
public void afterPropertiesSet() throws Exception {
/*
* 建議此處使用HttpClients.custom的方式來創(chuàng)建HttpClientBuilder,而不要使用HttpClientBuilder.create()方法來創(chuàng)建HttpClientBuilder
* 從官方文檔可以得出,HttpClientBuilder是非線程安全的,但是HttpClients確實(shí)Immutable的,immutable 對(duì)象不僅能夠保證對(duì)象的狀態(tài)不被改變,
* 而且還可以不使用鎖機(jī)制就能被其他線程共享
*/
this.client = HttpClients.custom().setConnectionManager(poolHttpcConnManager)
.setRetryHandler(httpRequestRetryHandler)
.setKeepAliveStrategy(connectionKeepAliveStrategy)
.setRoutePlanner(proxyRoutePlanner)
.setDefaultRequestConfig(config)
.build();
}
// 返回實(shí)例的類型
@Override
public CloseableHttpClient getObject() throws Exception {
return this.client;
}
@Override
public Class<?> getObjectType() {
return (this.client == null ? CloseableHttpClient.class : this.client.getClass());
}
// 構(gòu)建的實(shí)例為單例
@Override
public boolean isSingleton() {
return true;
}
}
七、增加配置文件
# 代理的host httpclient.config.proxyhost=xxx.xx.xx.xx # 代理端口 httpclient.config.proxyPort=8080 # 連接超時(shí)或異常重試次數(shù) httpclient.config.retryTime=3 # 長(zhǎng)連接保持時(shí)間,單位為s httpclient.config.keepAliveTime=30 # 連接池最大連接數(shù) httpclient.config.connMaxTotal=20 httpclient.config.maxPerRoute=20 # 連接超時(shí)時(shí)間,單位ms httpclient.config.connectTimeout=2000 # 請(qǐng)求超時(shí)時(shí)間 httpclient.config.connectRequestTimeout=2000 # sock超時(shí)時(shí)間 httpclient.config.socketTimeout=2000 # 連接存活時(shí)間,單位s httpclient.config.timeToLive=60
八、測(cè)試
測(cè)試代碼如下:
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實(shí)例
@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提交 請(qǐng)求 ,并用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);
}
}
通過上面的幾個(gè)步驟,就基本上完成了對(duì)HttpClient的封裝,如果需要更細(xì)致的話,可以按照上面的思路,逐步完善,將HttpClient封裝成HttpClientTemplate,因?yàn)镃loseableHttpClient內(nèi)部使用了回調(diào)機(jī)制,和JdbcTemplate,或者是RedisTemplate類似,直到可以以spring boot starter的方式提供服務(wù)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
springmvc+ajax+formdata上傳圖片代碼實(shí)例
這篇文章主要介紹了springmvc+ajax+formdata上傳圖片代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
Springboot實(shí)現(xiàn)VNC的反向代理功能
這篇文章主要介紹了Springboot實(shí)現(xiàn)VNC的反向代理,搭建過程也很簡(jiǎn)單,通過注冊(cè)bean攔截指定URL路徑進(jìn)行自定義操作,具體實(shí)例代碼跟隨小編一起看看需要的朋友可以參考下2021-09-09
命令行使用支持?jǐn)帱c(diǎn)續(xù)傳的java多線程下載器
java命令行下載器,支持?jǐn)帱c(diǎn)續(xù)傳下載,多線程下載,需要的朋友可以參考下2014-02-02
Java實(shí)現(xiàn)圖片上文字內(nèi)容的動(dòng)態(tài)修改的操作步驟
在數(shù)字圖像處理領(lǐng)域,Java提供了強(qiáng)大的庫來處理圖片,包括讀取、修改和寫入圖片,如果你需要在Java應(yīng)用程序中修改圖片上的文字內(nèi)容,可以通過圖像處理技術(shù)來實(shí)現(xiàn),這篇博文將介紹如何使用Java實(shí)現(xiàn)圖片上文字內(nèi)容的動(dòng)態(tài)修改,需要的朋友可以參考下2024-07-07
java開發(fā)MyBatis中常用plus實(shí)體類注解符詳解
這篇文章主要為大家介紹了java開發(fā)MyBatis常用的plus實(shí)體類注解符示例應(yīng)用詳解有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10
Java中的instanceof關(guān)鍵字在Android中的用法實(shí)例詳解
instanceof是Java的一個(gè)二元操作符,和==,>,<是同一類東西。接下來通過本文給大家介紹Java中的instanceof關(guān)鍵字在Android中的用法,非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-07-07

