前言
若要透過 Java 呼叫 API 端點, 可透過多種不同的程式工具進行呼叫,像是 Apache HTTP Client 、OkHTTP、WevFlux 等。今天將介紹使用 Apache HTTP Client 開發所遇到的坑,以及對應的解決辦法。
發生原因
由於因業務邏輯需求要呼叫多次 API 。可能會重複使用建立的 HTTP Client Instance 來去呼叫 API, 若 API 的時間連線時間太長, 可能會出現 TCP Idle connection , 導致程式無法順利執行完成,拋出 Socket Exception: Connection Reset 問題
以下為發生的程式碼範例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static void main(String[] args) throws IOException { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); List<String> urlList = new ArrayList<>(); try (CloseableHttpClient httpClient = httpClientBuilder.build()) { for (String url : urlList) {
HttpGet getRequest = new HttpGet(url); try (CloseableHttpResponse response = httpClient.execute(getRequest)) { HttpEntity entity = response.getEntity(); if (entity != null && response.getStatusLine().getStatusCode() == 200) { String result = EntityUtils.toString(entity); } } catch (IOException e) { log.error(e.getMessage(), e); } } } catch (IOException e) { log.error(e.getMessage(), e); } }
|
解決辦法
解法一
單次 request 就 重新 new 一個 CloseableHttpClient
。
然後使用完畢後就 close (try with resources)。這樣的做法為每次進行呼叫 API 都重新進行一次 TCP Connection。
依照官法文件說法,每次重新建立一個新的 HTTP Client Instance 是成本很高的事情,因此假設程式若有效能需求的情況下。此解決方法可能不盡理想
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpEntity; 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.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils;
import java.io.IOException; import java.util.ArrayList; import java.util.List;
@Slf4j public class HttpClientExample { public static void main(String[] args) throws IOException { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
List<String> urlList = new ArrayList<>(); for (String url : urlList) { try (CloseableHttpClient httpClient = httpClientBuilder.build()) { HttpGet getRequest = new HttpGet(url); try (CloseableHttpResponse response = httpClient.execute(getRequest)) { HttpEntity entity = response.getEntity(); if (entity != null && response.getStatusLine().getStatusCode() == 200) { String result = EntityUtils.toString(entity); } } catch (IOException e) { log.error(e.getMessage(), e); }
} catch (IOException e) { log.error(e.getMessage(), e); } }
} }
|
解法二
使用 connection pool 的方式來驗證 connection 是否為 idle connection, 同時也能減少多次呼叫 API 時所吃的資源與效能。
設定 Connection Pool 參數來定期驗證 TCP Connection 是否為過期或無效的 Connection。
1 2 3 4 5 6 7 8 9
| PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setValidateAfterInactivity(500); CloseableHttpClient httpclient = HttpClients.custom() .setConnectionManager(cm) .evictExpiredConnections() .evictIdleConnections(5L, TimeUnit.SECONDS) .setRetryHandler(DefaultHttpRequestRetryHandler.INSTANCE) .build();
|
補充說明
max per route : connection manager 預設每個 domain 最大的 connection 數量為 5
evictExpiredConnections 與 evictIdleConnections 用於設置在背景中清理過期的 connection
setValidateAfterInactivity : 每次取得連線時,假設該連線空閒超過該時間,則會驗證是否可用。默認值為 2000ms
設定重試機制:預設試 3 次,但假設 pool 中的 max per route 是五個 connection , 可能還是會出現例外。最好的保險是重是次數需大於 MaxPerRoute , 保證都失效後,重新進行連接
參考資料