同步調用第三方 API,基於 HTTP / HTTPS 協定,請求 POST 、 GET 、 PUT 、 DELETE 等方式,並且提供設置連線數量、讀取逾時等功能,EP 22 增加了相依套件及範例,提供路由設定,重試處理器,並透過 JUnit 5 單元測試來驗證產出結果。
Table of Contents
Toggle前言
HttpClient 是一套支援 HTTP 協議的用戶端程式庫,實現了所有 HTTP 的方法,如: GET 、 POST 、PUT 等,以及支援自動轉向與代理服務器等,提供了許多高效率的類別。
Spring Boot HttpClient
檔案目錄
./
+- build.gradle
+- src
+- main
+- resources
| +- application.properties
+- java
| +- org
| +- ruoxue
| +- spring_boot_168
| +- config
| +- HttpClientConfig.java
Gradle
build.gradle
增加 HttpClient。
排除 Logging,因有使用 Log4j2,若沒使用 Log4j2 則保留 Logging 。
修改完後,點右鍵,Gradle -> Refresh Gradle Project 。
buildscript {
group 'org.ruoxue.spring-boot-168'
version = '0.0.1-SNAPSHOT'
ext {
springBootVersion = '2.1.7.RELEASE'
httpClientVersion = '4.5.13'
}
}
dependencies {
implementation ("org.apache.httpcomponents:httpclient:${httpClientVersion}") {
exclude group: 'commons-logging', module: 'commons-logging'
}
implementation ("org.apache.httpcomponents:httpmime:${httpClientVersion}") {
exclude group: 'commons-logging', module: 'commons-logging'
}
}
設定
application.properties
增加 HttpClient 設定。
http-client.maxTotal=500
http-client.defaultMaxPerRoute=100
http-client.connectTimeout=3000
http-client.connectionRequestTimeout=3000
http-client.socketTimeout=3000
http-client.staleConnectionCheckEnabled=true
http-client.validateAfterInactivity=3000
http-client.retryEnable=true
http-client.retryCount=3
http-client.retryInterval=1000
http-client.routeEnable=true
http-client.routeList[0].hostname=aaa.cc
http-client.routeList[0].maxConnection=400
http-client.routeList[1].hostname=bbb.cc
http-client.routeList[1].maxConnection=50
http-client.routeList[2].hostname=ccc.cc
http-client.routeList[2].maxConnection=50
HttpClientConfig.java
新增檔案,讀取 application.properties ,前綴為 http-client 開頭,設定連線池大小、連線逾時、開啟重試、路由設定等。
package org.ruoxue.spring_boot_168.config;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLException;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.routing.HttpRoute;
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.apache.http.protocol.HttpContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ConfigurationProperties("http-client")
@Configuration
@Slf4j
public class HttpClientConfig {
@Value("${http-client.maxTotal}")
private int maxTotal;
@Value("${http-client.defaultMaxPerRoute}")
private int defaultMaxPerRoute;
@Value("${http-client.connectTimeout}")
private int connectTimeout;
@Value("${http-client.connectionRequestTimeout}")
private int connectionRequestTimeout;
@Value("${http-client.socketTimeout}")
private int socketTimeout;
@Value("${http-client.validateAfterInactivity}")
private int validateAfterInactivity;
@Value("${http-client.retryEnable}")
private boolean retryEnable;
@Value("${http-client.retryCount}")
private int retryCount;
@Value("${http-client.retryInterval}")
private int retryInterval;
@Value("${http-client.routeEnable}")
private boolean routeEnable;
private final List<Route> routeList = new ArrayList<>();
@Bean(name = "poolingHttpClientConnectionManager")
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
connectionManager.setValidateAfterInactivity(validateAfterInactivity);
if (routeEnable) {
routeList.forEach(e -> {
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(e.getHostname())), e.getMaxConnection());
});
}
return connectionManager;
}
@Bean(name = "requestConfigBuilder")
public RequestConfig.Builder requestConfigBuilder() {
RequestConfig.Builder builder = RequestConfig.custom();
return builder.setConnectTimeout(connectTimeout) //
.setConnectionRequestTimeout(connectionRequestTimeout) //
.setSocketTimeout(socketTimeout);
}
@Bean(name = "requestConfig")
public RequestConfig requestConfig(@Qualifier("requestConfigBuilder") RequestConfig.Builder builder) {
return builder.build();
}
@Bean(name = "httpClientBuilder")
public HttpClientBuilder httpClientBuilder(
@Qualifier("poolingHttpClientConnectionManager") PoolingHttpClientConnectionManager poolingHttpClientConnectionManager,
@Qualifier("requestConfig") RequestConfig requestConfig) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
if (retryEnable) {
DefaultHttpRequestRetryHandler requestRetryHandler = new DefaultHttpRequestRetryHandler(retryCount, true,
new ArrayList<>()) {
public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
log.info("Retry request, execution count: {}, exception: {}", executionCount, exception);
if (exception instanceof SocketTimeoutException) {
return false;
}
if (exception instanceof UnknownHostException) {
return false;
}
if (exception instanceof SSLException) {
return false;
}
if (executionCount >= retryCount) {
return false;
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
return super.retryRequest(exception, executionCount, context);
}
};
httpClientBuilder.setRetryHandler(requestRetryHandler);
}
httpClientBuilder.setDefaultRequestConfig(requestConfig);
return httpClientBuilder;
}
@Bean(name = "closeableHttpClient")
public CloseableHttpClient closeableHttpClient(
@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder) {
return httpClientBuilder.build();
}
public List<Route> getRouteList() {
return routeList;
}
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public static class Route {
/** IP or DNS name */
private String hostname;
/** 最大連線數 */
private int maxConnection;
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.JSON_STYLE);
builder.appendSuper(super.toString());
builder.append("hostname", hostname);
builder.append("maxConnection", maxConnection);
return builder.toString();
}
}
}
Http Client 快速設定
http-client.maxTotal=500
http-client.defaultMaxPerRoute=100
http-client.connectTimeout=3000
http-client.connectionRequestTimeout=3000
http-client.socketTimeout=3000
測試 JUnit 5
HttpClientConfigTest.java
新增單元測試,驗證是否符合預期 。
package org.ruoxue.spring_boot_168.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.ruoxue.spring_boot_168.Application;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class HttpClientConfigTest {
@Autowired
private HttpClientConfig config;
@Test
public void config() {
assertNotNull(config);
System.out.println("maxTotal: " + config.poolingHttpClientConnectionManager().getMaxTotal());
System.out.println("routeList: " + config.getRouteList());
assertEquals(3, config.getRouteList().size());
}
}
config
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
心得分享
maxTotal: 500
routeList: [{"hostname":"aaa.cc","maxConnection":400}, {"hostname":"bbb.cc","maxConnection":50}, {"hostname":"ccc.cc","maxConnection":50}]
網路連線,常常會發生無法預期不可控的狀況,而 connectionRequestTimeout 、connectTimeout 、socketTimeout 這些參數,請依當時的場景做適當的調整,也就是經過不斷的反覆測試,才能找到較合適的設定,其單位都是毫秒。
Client Http
除了 HttpClient 之外,還有採用異步的方式, AsyncHttpClient 可以評估使用。