HttpClient通用Builder连接池

820 阅读6分钟

场景

最近公司大量使用了第三方的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);

    }

}