異步調用第三方接口,提供了連線池管理、設定連線、讀取逾時等功能, EP 23 增加了相依套件及範例,提供路由設定,並透過 JUnit 5 單元測試來驗證產出結果。
Table of Contents
Toggle前言
HttpAsyncClient 是一套支援 HTTP 協議的用戶端程式庫, 使用了異步回調的方式,實現了所有 HTTP 的方法,如: GET、POST、PUT 等,適用於處理大量併行連線的場景。
Spring Boot HttpAsyncClient
檔案目錄
./
+- build.gradle
+- src
+- main
+- resources
| +- application.properties
+- java
| +- org
| +- ruoxue
| +- spring_boot_168
| +- config
| +- HttpAsyncClientConfig.java
Gradle
build.gradle
增加 HttpAsyncClient。
排除 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'
httpAsyncClientVersion = '4.1.5'
}
}
dependencies {
implementation ("org.apache.httpcomponents:httpasyncclient:${httpAsyncClientVersion}") {
exclude group: 'commons-logging', module: 'commons-logging'
}
}
設定
application.properties
Http Async Client
http-async-client.maxTotal=500
http-async-client.defaultMaxPerRoute=100
http-async-client.connectTimeout=3000
http-async-client.connectionRequestTimeout=3000
http-async-client.socketTimeout=3000
http-async-client.staleConnectionCheckEnabled=true
http-async-client.routeEnable=true
http-async-client.routeList[0].hostname=aaa.cc
http-async-client.routeList[0].maxConnection=400
http-async-client.routeList[1].hostname=bbb.cc
http-async-client.routeList[1].maxConnection=50
http-async-client.routeList[2].hostname=ccc.cc
http-async-client.routeList[2].maxConnection=50
HttpAsyncClientConfig.java
新增檔案,讀取 application.properties ,前綴為 http-async-client 開頭,設定連線池大小、連線逾時、開啟重試、路由設定等。
package org.ruoxue.spring_boot_168.config;
import java.util.ArrayList;
import java.util.List;
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.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
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-async-client")
@Configuration
@Slf4j
public class HttpAsyncClientConfig {
@Value("${http-async-client.maxTotal}")
private int maxTotal;
@Value("${http-async-client.defaultMaxPerRoute}")
private int defaultMaxPerRoute;
@Value("${http-async-client.connectTimeout}")
private int connectTimeout;
@Value("${http-async-client.connectionRequestTimeout}")
private int connectionRequestTimeout;
@Value("${http-async-client.socketTimeout}")
private int socketTimeout;
@Value("${http-async-client.routeEnable}")
private boolean routeEnable;
private final List<Route> routeList = new ArrayList<>();
@Bean(name = "poolingNHttpClientConnectionManager")
public PoolingNHttpClientConnectionManager poolingNHttpClientConnectionManager() {
IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
.setIoThreadCount(200 * Runtime.getRuntime().availableProcessors()) //
.setSoKeepAlive(true) //
.build();
ConnectingIOReactor ioReactor = null;
try {
ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
} catch (IOReactorException ex) {
log.error(ex.getMessage(), ex);
}
PoolingNHttpClientConnectionManager connectionManager = new PoolingNHttpClientConnectionManager(ioReactor);
connectionManager.setMaxTotal(maxTotal);
connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
if (routeEnable) {
routeList.forEach(e -> {
connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost(e.getHostname())), e.getMaxConnection());
});
}
return connectionManager;
}
@Bean(name = "asyncRequestConfigBuilder")
public RequestConfig.Builder asyncRequestConfigBuilder() {
RequestConfig.Builder builder = RequestConfig.custom();
return builder.setConnectTimeout(connectTimeout) //
.setConnectionRequestTimeout(connectionRequestTimeout) //
.setSocketTimeout(socketTimeout);
}
@Bean(name = "asyncRequestConfig")
public RequestConfig requestConfig(@Qualifier("asyncRequestConfigBuilder") RequestConfig.Builder builder) {
return builder.build();
}
@Bean(name = "asyncHttpClientBuilder")
public HttpAsyncClientBuilder asyncHttpClientBuilder(
@Qualifier("poolingNHttpClientConnectionManager") PoolingNHttpClientConnectionManager poolingNHttpClientConnectionManager,
@Qualifier("asyncRequestConfig") RequestConfig requestConfig) {
HttpAsyncClientBuilder httpAsyncClientBuilder = HttpAsyncClientBuilder.create();
httpAsyncClientBuilder.setConnectionManager(poolingNHttpClientConnectionManager);
httpAsyncClientBuilder.setDefaultRequestConfig(requestConfig);
return httpAsyncClientBuilder;
}
@Bean(name = "closeableHttpAsyncClient")
public CloseableHttpAsyncClient closeableHttpAsyncClient(
@Qualifier("asyncHttpClientBuilder") HttpAsyncClientBuilder asyncHttpClientBuilder) {
return asyncHttpClientBuilder.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();
}
}
}
測試 JUnit 5
HttpAsyncClientConfigTest.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 HttpAsyncClientConfigTest {
@Autowired
private HttpAsyncClientConfig config;
@Test
public void config() {
assertNotNull(config);
System.out.println("maxTotal: " + config.poolingNHttpClientConnectionManager().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}]
心得分享
Async Http Client
使用異步或同步連線,並不是絕對的選項,尤其當高併發連線請求時,異步方式往往很容易大量快速地,將連線池的連線消耗殆盡,有可能讓之後的請求,都將無法從連線池取得連線,造成許多連線逾時的錯誤,因此請依當時的場景,做不斷的調校與測試,才能夠找出合適的解決方案。