Spring Boot HttpAsyncClient - Spring Boot 168 EP 23

Spring Boot HttpAsyncClient – Spring Boot 168 EP 23

異步調用第三方接口,提供了連線池管理、設定連線、讀取逾時等功能, EP 23 增加了相依套件及範例,提供路由設定,並透過 JUnit 5 單元測試來驗證產出結果。

前言

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

使用異步或同步連線,並不是絕對的選項,尤其當高併發連線請求時,異步方式往往很容易大量快速地,將連線池的連線消耗殆盡,有可能讓之後的請求,都將無法從連線池取得連線,造成許多連線逾時的錯誤,因此請依當時的場景,做不斷的調校與測試,才能夠找出合適的解決方案。

發佈留言