本文為博主原創,轉載請注明出處:
項目中存在第三方系統之間的服務調用通信,且會進行頻繁調用,由於很早之前實現的調用方式為每調用一次外部接口,就需要新建一個HttpClient 對象。由於頻繁調用,會存在性能問題。
針對這種場景,進行優化,使用httpClient 連接池,避免重復頻繁創建httpClient 造成性能問題。以下為簡單實現的demo:
1. 對 httpClient 的屬性及常用配置封裝 HttpPoolProperties
package com.example.demo.config; import lombok.Data; import org.springframework.stereotype.Component; @Component //@ConfigurationProperties(prefix = "http.pool.conn") // 可在配置文件中進行配置 @Data public class HttpPoolProperties { // 最大連接數 private Integer maxTotal = 20; // 同路由並發數 private Integer defaultMaxPerRoute =20 ; private Integer connectTimeout = 2000; private Integer connectionRequestTimeout=2000; private Integer socketTimeout= 2000; // 線程空閑多久后進行校驗 private Integer validateAfterInactivity= 2000; // 重試次數 private Integer retryTimes = 2; // 是否開啟充實 private boolean enableRetry = true; // 重試的間隔:可實現 ServiceUnavailableRetryStrategy 接口 private Integer retryInterval= 2000; }
2. 創建httpClient 連接池,並對RestTemplate 指定httpClient 及連接池
package com.example.demo.util; import com.example.demo.config.HttpPoolProperties; import org.apache.http.client.config.RequestConfig; 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.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.RestTemplate; @Configuration public class HttpClientPoolUtils { @Autowired private HttpPoolProperties httpPoolProperties; /** * 首先實例化一個連接池管理器,設置最大連接數、並發連接數 * @return */ @Bean(name = "httpClientConnectionManager") public PoolingHttpClientConnectionManager getHttpClientConnectionManager(){ Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); PoolingHttpClientConnectionManager httpClientConnectionManager = new PoolingHttpClientConnectionManager(registry); //最大連接數 httpClientConnectionManager.setMaxTotal(httpPoolProperties.getMaxTotal()); //並發數 httpClientConnectionManager.setDefaultMaxPerRoute(httpPoolProperties.getDefaultMaxPerRoute()); httpClientConnectionManager.setValidateAfterInactivity(httpPoolProperties.getValidateAfterInactivity()); return httpClientConnectionManager; } /** * 實例化連接池,設置連接池管理器。 * 這里需要以參數形式注入上面實例化的連接池管理器 * @param httpClientConnectionManager * @return */ @Bean(name = "httpClientBuilder") public HttpClientBuilder getHttpClientBuilder(@Qualifier("httpClientConnectionManager")PoolingHttpClientConnectionManager httpClientConnectionManager){ //HttpClientBuilder中的構造方法被protected修飾,所以這里不能直接使用new來實例化一個HttpClientBuilder,可以使用HttpClientBuilder提供的靜態方法create()來獲取HttpClientBuilder對象 HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); httpClientBuilder.setConnectionManager(httpClientConnectionManager); if (httpPoolProperties.isEnableRetry()){ // 重試次數 httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(httpPoolProperties.getRetryTimes(), true)); // 若需要自定義http 的重試策略,可以重新實現ServiceUnavailableRetryStrategy 或 HttpRequestRetryHandler接口,比如對指定異常或制定狀態碼進行重試,並指定充實的次數。 }else { httpClientBuilder.disableAutomaticRetries(); } // 另外httpClientBuilder 可以設置長連接策略,dns解析器,代理,攔截器以及UserAgent等等。可根據業務需要進行實現 return httpClientBuilder; } /* 注入連接池,用於獲取httpClient * @param httpClientBuilder * @return */ @Bean("httpClient") public CloseableHttpClient httpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder){ return httpClientBuilder.build(); } /** * Builder是RequestConfig的一個內部類 * 通過RequestConfig的custom方法來獲取到一個Builder對象 * 設置builder的連接信息 * 這里還可以設置proxy,cookieSpec等屬性。有需要的話可以在此設置 * @return */ @Bean(name = "builder") public RequestConfig.Builder getBuilder(){ RequestConfig.Builder builder = RequestConfig.custom(); return builder.setConnectTimeout(httpPoolProperties.getConnectTimeout()) //連接上服務器(握手成功)的時間,超出拋出connect timeout //從連接池中獲取連接的超時時間,超時間未拿到可用連接,會拋出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool .setConnectionRequestTimeout(httpPoolProperties.getConnectionRequestTimeout()) //服務器返回數據(response)的時間,超過拋出read timeout .setSocketTimeout(httpPoolProperties.getSocketTimeout()); } /** * 使用builder構建一個RequestConfig對象 * @param builder * @return */ @Bean public RequestConfig getRequestConfig(@Qualifier("builder") RequestConfig.Builder builder){ return builder.build(); } /** * RestTemplate 指定httpClient 及連接池 * * @param httpClient * @return */ @Bean(name = "httpClientTemplate") public RestTemplate restTemplate(@Qualifier("httpClient") CloseableHttpClient httpClient) { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setHttpClient(httpClient); RestTemplate restTemplate = new RestTemplate(); restTemplate.setRequestFactory(factory); restTemplate.getMessageConverters().add(new StringHttpMessageConverter()); return restTemplate; } }
3。 創建清理線程對httpClient 空閑線程,失效線程進行清理
package com.example.demo.util; import org.apache.http.conn.HttpClientConnectionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class IdleConnectionEvictor extends Thread { @Autowired private HttpClientConnectionManager connMgr; private volatile boolean shutdown; public IdleConnectionEvictor() { super(); super.start(); } @Override public void run() { try { while (!shutdown) { synchronized (this) { wait(5000); // 關閉失效的連接 connMgr.closeExpiredConnections(); } } } catch (InterruptedException ex) { // 結束 } } //關閉清理無效連接的線程 public void shutdown() { shutdown = true; synchronized (this) { notifyAll(); } } }
4. 單元測試
package com.example.demo; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.web.client.RestTemplate; import java.io.IOException; @Slf4j @SpringBootTest public class HttpTest { @Autowired private RestTemplate httpClientTemplate; @Autowired private CloseableHttpClient httpClient; @Test void test() throws IOException {
String result = httpClientTemplate.getForObject("https://www.baidu.com/",String.class); System.out.println("httpClientTemplate==="+result);
// 聲明 http get 請求 String url = "https://www.baidu.com/"; HttpGet httpGet = new HttpGet(url); // 發起請求 CloseableHttpResponse response = this.httpClient.execute(httpGet); System.out.println("httpClient==="+response); } }
測試方法分別通過CloseableHttpClient httpClient 進行http調用與自定義的RestTemplate httpClientTemplate 進行 http 調用。
執行結果如下: