场景
最近公司大量使用了第三方的API接口,为了维护http请求,引入了http-clinet依赖,进行http调用。但是,发现没有一个适合的工具类,自己进行了二次封装。
连接池优势
- HTTP连接池通过复用HTTP连接,避免了每次发起请求时都需要进行TCP的三次握手和四次挥手的过程,从而显著降低了请求响应的时间
- 自动管理TCP连接的方式,不仅节省了TCP连接建立和释放的时间,还避免了因为频繁创建和销毁连接而导致的系统资源浪费
- HTTP连接池支持更大的并发性能,因为在高并发情况下,系统端口资源很快会被耗尽,如果没有连接池,每次连接都会打开一个端口,而连接池可以有效地管理端口资源,避免系统因为端口资源耗尽而无法建立新的连接
代码实现
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
创建yml配置类
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* @author 苦瓜不苦
* @date 2023/12/16 0:51
**/
@Data
@Configuration
@ConfigurationProperties("spring-core.http-client")
public class HttpClientProperties {
/**
* 是否启用http-client
*/
private boolean enable = false;
/**
* 连接超时时间,单位:毫秒
*/
private int connectTimeout = 3000;
/**
* 读取超时时间,单位:毫秒
*/
private int readTimeout = 5000;
/**
* 从连接池获取连接时间,单位:毫秒
*/
private int connectionRequestTimeout = 2000;
/**
* 最多同时连接请求数
*/
private int maxTotal = 150;
/**
* 每个路由最大连接数
*/
private int maxPerRoute = 30;
/**
* 字符集
*/
private Charset charset = StandardCharsets.UTF_8;
}
重写HttpEntityEnclosingRequestBase对象
import lombok.Getter;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.springframework.http.HttpMethod;
import java.net.URI;
/**
* @author 苦瓜不苦
* @date 2023/12/22 22:09
**/
public class HttpObject extends HttpEntityEnclosingRequestBase {
private final HttpMethod httpMethod;
@Getter
private final String url;
public HttpObject(HttpMethod httpMethod, String url) {
this.httpMethod = httpMethod;
this.setURI(URI.create(url));
this.url = url;
}
@Override
public String getMethod() {
return this.httpMethod.name();
}
}
初始化HttpClient交给IOC管理
import lombok.extern.slf4j.Slf4j;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* HTTP连接池
*
* @author 苦瓜不苦
* @date 2024/1/3 17:51
**/
@Slf4j
@Configuration
@ConditionalOnClass(CloseableHttpClient.class)
@ConditionalOnProperty(prefix = "spring-core.http-client", name = "enable", havingValue = "true")
public class HttpClientConfigurer {
@Bean
public CloseableHttpClient httpClient(HttpClientProperties httpClientProperties) throws Exception {
int maxTotal = httpClientProperties.getMaxTotal();
int maxPerRoute = httpClientProperties.getMaxPerRoute();
int connectionRequestTimeout = httpClientProperties.getConnectionRequestTimeout();
int connectTimeout = httpClientProperties.getConnectTimeout();
int readTimeout = httpClientProperties.getReadTimeout();
// 创建和配置SSL连接工厂
SSLConnectionSocketFactory socketFactory = createSocketFactory();
// 创建注册表
Registry<ConnectionSocketFactory> registry = createSocketFactoryRegistry(socketFactory);
// HTTP连接池对象
PoolingHttpClientConnectionManager connectionManager = createConnectionManager(registry, maxTotal, maxPerRoute);
// 超时设置
RequestConfig config = createRequestConfig(connectionRequestTimeout, connectTimeout, readTimeout);
// 创建连接池
return HttpClients.custom().setRetryHandler(retryHandler()).setConnectionManager(connectionManager).setDefaultRequestConfig(config).build();
}
/**
* 重试机制
*
* @return
*/
private HttpRequestRetryHandler retryHandler() {
return (exception, executionCount, context) -> {
if (executionCount >= 3) {
return false;
}
if (exception instanceof NoHttpResponseException) {
log.info("第 {} 次, 发起重试机制", executionCount);
return true;
}
return false;
};
}
/**
* 创建和配置SSL连接工厂
*
* @return
* @throws NoSuchAlgorithmException
*/
private SSLConnectionSocketFactory createSocketFactory() throws NoSuchAlgorithmException {
// return new SSLConnectionSocketFactory(SSLContext.getDefault(), NoopHostnameVerifier.INSTANCE);
try {
SSLContext sc = SSLContext.getInstance("TLS");
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sc.init(null, new TrustManager[]{trustManager}, null);
return new SSLConnectionSocketFactory(sc, NoopHostnameVerifier.INSTANCE);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 创建注册表
*
* @param socketFactory SSL工厂对象
* @return
*/
private Registry<ConnectionSocketFactory> createSocketFactoryRegistry(SSLConnectionSocketFactory socketFactory) {
return RegistryBuilder.<ConnectionSocketFactory>create().register("https", socketFactory).register("http", PlainConnectionSocketFactory.INSTANCE).build();
}
/**
* 连接池管理器
*
* @param registry 注册表
* @param maxTotal 最多同时连接请求数
* @param maxPerRoute 每个路由最大连接数,路由指IP+PORT或者域名,例如连接池大小(MaxTotal)设置为300,路由连接数设置为200(DefaultMaxPerRoute),对于www.a.com与www.b.com两个路由来说,发起服务的主机连接到每个路由的最大连接数(并发数)不能超过200,两个路由的总连接数不能超过300。
* @return
*/
private PoolingHttpClientConnectionManager createConnectionManager(Registry<ConnectionSocketFactory> registry, int maxTotal, int maxPerRoute) {
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(registry);
connManager.setMaxTotal(maxTotal);
connManager.setDefaultMaxPerRoute(maxPerRoute);
return connManager;
}
/**
* 超时设置
*
* @param connectionRequestTimeout 从连接池获取连接时间
* @param connectTimeout 创建连接时间
* @param readTimeout 数据传输时间
* @return
*/
private RequestConfig createRequestConfig(int connectionRequestTimeout, int connectTimeout, int readTimeout) {
return RequestConfig.custom().setConnectionRequestTimeout(connectionRequestTimeout).setConnectTimeout(connectTimeout).setSocketTimeout(readTimeout).build();
}
}
管理HttpClient的Builder对象
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.credlink.core.configurer.HttpClientConfigurer;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.http.HttpMethod;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* HTTP连接池客户端
*
* @author 苦瓜不苦
* @date 2024/1/16 11:30
**/
@Slf4j
@Component
@ConditionalOnBean(HttpClientConfigurer.class)
public class SpringHttpClient {
private final CloseableHttpClient httpClient;
public SpringHttpClient(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
}
public Builder builder() {
return new Builder(httpClient);
}
public static class Builder {
// Http客户端
private final CloseableHttpClient httpClient;
// HTTP对象
private HttpObject httpBase;
// 表单内容
private Map<String, Object> formMap;
// JSON内容
private String bodyJson;
// 请求头
private Map<String, Object> headerMap;
// 字符集
private Charset charset;
// 请求配置,单独设置超时时间
private RequestConfig requestConfig;
// 请求开始时间
private final long millis;
// 是否打印响应内容
private boolean isLog;
public Builder(CloseableHttpClient httpClient) {
this.httpClient = httpClient;
this.millis = System.currentTimeMillis();
this.charset = StandardCharsets.UTF_8;
}
public Builder get(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.GET, uri);
return this;
}
public Builder post(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.POST, uri);
return this;
}
public Builder put(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.PUT, uri);
return this;
}
public Builder delete(@NonNull String uri) {
this.httpBase = new HttpObject(HttpMethod.DELETE, uri);
return this;
}
public Builder formMap(@NonNull Map<String, Object> formMap) {
if (Objects.isNull(this.formMap)) {
this.formMap = new HashMap<>();
}
this.formMap.putAll(formMap);
return this;
}
public Builder form(@NonNull String name, Object value) {
if (Objects.isNull(this.formMap)) {
this.formMap = new HashMap<>();
}
this.formMap.put(name, value);
return this;
}
public Builder bodyJson(@NonNull Object bodyJson) {
this.bodyJson = (bodyJson instanceof String) ? bodyJson.toString() : JSONObject.toJSONString(bodyJson);
return this;
}
public Builder headerMap(@NonNull Map<String, Object> headerMap) {
if (Objects.isNull(this.headerMap)) {
this.headerMap = new HashMap<>();
}
this.headerMap.putAll(headerMap);
return this;
}
public Builder header(@NonNull String name, Object value) {
if (Objects.isNull(this.headerMap)) {
this.headerMap = new HashMap<>();
}
this.headerMap.put(name, value);
return this;
}
public Builder charset(@NonNull Charset charset) {
this.charset = charset;
return this;
}
public Builder isLog() {
this.isLog = true;
return this;
}
public Builder channel(ChannelResult channelResult) {
this.channelResult = channelResult;
return this;
}
public Result build() {
HttpEntity entity = null;
Result result = new Result();
result.setLog(isLog);
result.setCharset(charset);
try {
// 设置表单请求参数
if (Objects.nonNull(formMap)) {
URIBuilder builder = new URIBuilder(httpBase.getUrl());
builder.setCharset(charset);
formMap.forEach((k, v) -> {
if (Objects.nonNull(v)) {
builder.setParameter(k, String.valueOf(v));
}
});
URI uri = builder.build();
httpBase.setURI(uri);
}
// 设置JSON请求参数
if (Objects.nonNull(bodyJson)) {
ByteArrayEntity byteArray = new ByteArrayEntity(bodyJson.getBytes(charset), ContentType.APPLICATION_JSON);
httpBase.setEntity(byteArray);
}
// 设置超时时间
if (Objects.nonNull(requestConfig)) {
httpBase.setConfig(requestConfig);
}
// 设置请求头
if (Objects.nonNull(headerMap)) {
for (Map.Entry<String, Object> entry : headerMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (Objects.nonNull(value)) {
httpBase.setHeader(key, String.valueOf(value));
}
}
}
// 发送请求
CloseableHttpResponse response = httpClient.execute(httpBase);
result.setCode(response.getStatusLine().getStatusCode());
entity = response.getEntity();
result.setContentType(ContentType.get(entity));
result.setBytes(EntityUtils.toByteArray(entity));
} catch (URISyntaxException | IOException e) {
throw new RuntimeException(e);
} finally {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
log.info("释放连接异常\n", e);
}
log.info("请求地址 {}, 总耗时 {} 毫秒", httpBase.getUrl(), (System.currentTimeMillis() - millis));
}
return result;
}
}
@Getter
@Setter
public static class Result {
// HTTP状态码
private Integer code;
// 响应字节
private byte[] bytes;
// 内容类型
private ContentType contentType;
// 是否打印日志
private boolean isLog;
// 字符集
private Charset charset;
public boolean isSuccess() {
return Objects.nonNull(code) && code == 200;
}
public boolean isFile() {
return Objects.equals(ContentType.APPLICATION_OCTET_STREAM.getMimeType(), contentType.getMimeType());
}
public boolean isBytesNull() {
return Objects.isNull(bytes);
}
public String toJson() {
if (isBytesNull()) {
return null;
}
if (isFile()) {
throw new RuntimeException(String.format("内容类型 %s, 属于文件", contentType));
}
String body = new String(bytes, charset);
if (isLog) {
log.info("HTTP响应结果 {}", body);
}
return body;
}
public <T> T toBean(@NonNull Class<T> tClass) {
return JSONObject.parseObject(toJson(), tClass);
}
public <T> List<T> toList(@NonNull Class<T> tClass) {
return JSONArray.parseArray(toJson(), tClass);
}
public OutputStream toStream() {
if (isBytesNull()) {
return null;
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
bos.write(bytes);
return bos;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public File toFile(File file) {
if (isBytesNull()) {
return null;
}
if (!isFile()) {
throw new RuntimeException(String.format("内容类型 %s, 属于文本, 其数据 %s", contentType.toString(), toJson()));
}
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(bytes);
return file;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public File toFile(String file) {
return toFile(new File(file));
}
}
}
使用方式
只需要引入SpringHttpClient,调用里面的builder()方法,即可使用链式编程。支持post、get、put、delete的http请求和form-data、application/json的请求类型,打印响应日志以及http的耗时记录,其中toBean()方法可以转化成对应的实体类。
import com.alibaba.fastjson2.JSONObject;
import com.credlink.core.client.SpringHttpClient;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author 苦瓜不苦
* @date 2024/3/11 11:21
**/
@Slf4j
@SpringBootTest
public class LinkApiServerTest {
@Autowired
private SpringHttpClient httpClient;
@Test
void test() {
JSONObject params = new JSONObject();
params.put("age", 18);
JSONObject result = httpClient.builder()
.post("http://127.0.0.1:8080/api/student/query")
.header("Authorization", "Basic xxxxxxxxxx")
.bodyJson(params)
.build()
.toBean(JSONObject.class);
log.info("响应结果 {}", result);
}
}