Spring Boot HttpClient - Spring Boot 168 EP 22

Spring Boot HttpClient – Spring Boot 168 EP 22

同步調用第三方 API,基於 HTTP / HTTPS 協定,請求 POST 、 GET 、 PUT 、 DELETE 等方式,並且提供設置連線數量、讀取逾時等功能,EP 22 增加了相依套件及範例,提供路由設定,重試處理器,並透過 JUnit 5 單元測試來驗證產出結果。

前言

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  可以評估使用。

發佈留言