HttpClient 同步和異步工具類

依賴項

<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.9</version>
</dependency>
<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.5</version>
</dependency>
<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpasyncclient</artifactId>
            <version>4.1.5</version>
 </dependency>
 
 <!-- 將Java Util Logging重定向到slf4j -->
<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>1.7.10</version>
</dependency>
        <!-- 將log4j重定向到slf4j -->
<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.10</version>
</dependency>

異步工具類實現:

package idslilang.com.just.go.async;

import org.apache.commons.codec.Charsets;
import org.apache.http.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
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.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.SSLInitializationException;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class AsyncHttpClientUtil implements AutoCloseable {

    private static final Logger logger = LoggerFactory.getLogger(AsyncHttpClientUtil.class);

    private final CloseableHttpAsyncClient httpClient;
    private final PoolingNHttpClientConnectionManager connMgr;
    private final ThreadLocal<HttpClientContext> httpContext = ThreadLocal.withInitial(HttpClientContext::create);

    public AsyncHttpClientUtil() {

        // 自定義keep-alive策略,keep-alive使得tcp連接可以被複用。
        // 但默認的keep-alive時長爲無窮大,不符合現實,所以需要改寫,定義一個更短的時間
        // 如果服務器http響應頭包含 "Keep-Alive:timeout=" ,則使用timeout=後面指定的值作爲keep-alive的時長,否則默認60秒
        ConnectionKeepAliveStrategy strategy = (httpResponse, httpContext) -> {
            HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    return Long.parseLong(value) * 1000;
                }
            }

            return 60 * 1000;

        };
        //設置協議http和https對應的處理socket鏈接工廠的對象 ,ssl
        SSLContext sslContext = null;
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            TrustStrategy noopTrustStrategy = new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            };
            sslContext = SSLContexts.custom().setProtocol(SSLConnectionSocketFactory.TLS)
                    .loadTrustMaterial(trustStore, noopTrustStrategy).build();
        } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException ex) {
            throw new SSLInitializationException(ex.getMessage(), ex);
        }
        Registry<SchemeIOSessionStrategy> sessionStrategy = RegistryBuilder.<SchemeIOSessionStrategy>create()
                .register("http", NoopIOSessionStrategy.INSTANCE)
                .register("https", new SSLIOSessionStrategy(sslContext, NoopHostnameVerifier.INSTANCE)).build();

        // 設置reactor參數
        IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
                .setIoThreadCount(Runtime.getRuntime().availableProcessors()).setSoKeepAlive(true).build();
        ConnectingIOReactor ioReactor;
        try {
            ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
        } catch (IOReactorException e) {
            throw new RuntimeException(e.getMessage(), e);
        }


        // 自定義連接池,連接池可以連接可以使連接可以被複用。
        // 連接的複用需要滿足:Keep-alive + 連接池。keep-alive使得連接能夠保持存活,不被系統銷燬;連接池使得連接可以被程序重複引用
        // 默認的連接池,DefaultMaxPerRoute只有5個,MaxTotal只有10個,不滿足實際的生產需求
        connMgr = new PoolingNHttpClientConnectionManager(ioReactor);

        // 最大連接數500
        connMgr.setMaxTotal(500);
        // 同一個ip:port的請求,最大連接數200
        connMgr.setDefaultMaxPerRoute(200);

        // 定義請求超時配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(60)  // 從連接池裏獲取連接的超時時間
                .setConnectTimeout(60)            // 建立TCP的超時時間
                .setSocketTimeout(60)             // 讀取數據包的超時時間
                .build();

        httpClient = HttpAsyncClients.custom()
                .setConnectionManager(connMgr)
                //開啓後臺線程清除閒置30秒以上的連接
                .setKeepAliveStrategy(strategy)
                .setDefaultRequestConfig(requestConfig)
                .build();
        httpClient.start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                close();
            } catch (IOException e) {
                logger.info("peace close fail");
            }
        }));


    }

    /**
     * 因爲HttpClient是線程安全的,可以提供給多個線程複用,同時連接池的存證的目的就是爲了可以複用連接,所以提供單例模式
     */
    private static class HttpClientUtilHolder {
        private static final AsyncHttpClientUtil INSTANCE = new AsyncHttpClientUtil();
    }

    public static AsyncHttpClientUtil getInstance() {
        return HttpClientUtilHolder.INSTANCE;
    }

    public PoolingNHttpClientConnectionManager getConnMgr() {
        return connMgr;
    }

    public HttpContext getHttpContext() {
        return httpContext.get();
    }

    public CloseableHttpAsyncClient getHttpClient() {
        return httpClient;
    }

    /**
     * http get操作
     *
     * @param url     請求地址
     * @param headers 請求頭
     * @return 返回響應內容
     */
    public CompletableFuture<String> get(String url, Map<String, String> headers) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        HttpGet httpGet = new HttpGet(url);
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpGet.setHeader(header.getKey(), header.getValue());
            }
        }
        httpClient.execute(httpGet, callback(completableFuture));
        return completableFuture;
    }

    /**
     * form表單post
     *
     * @param url     請求地址
     * @param params  參數內容
     * @param headers http頭信息
     * @return http文本格式響應內容
     */
    public CompletableFuture<String> post(String url, Map<String, String> params, Map<String, String> headers) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        HttpPost httpPost = new HttpPost(url);
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpPost.setHeader(header.getKey(), header.getValue());
            }
        }
        List<NameValuePair> formParams = new ArrayList<>();
        if (params != null) {
            for (Map.Entry<String, String> param : params.entrySet()) {
                formParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
            }
        }
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(formParams, Charsets.UTF_8);
        httpPost.setEntity(formEntity);
        httpClient.execute(httpPost, callback(completableFuture));
        return completableFuture;
    }

    /**
     * json內容post
     *
     * @param url     請求地址
     * @param data    json報文
     * @param headers http頭
     * @return http文本格式響應內容
     */
    public CompletableFuture<String> post(String url, String data, Map<String, String> headers) {
        CompletableFuture<String> completableFuture = new CompletableFuture<>();
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("Content-type", ContentType.APPLICATION_JSON.getMimeType());
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpPost.setHeader(header.getKey(), header.getValue());
            }
        }
        if (!StringUtils.isEmpty(data)) {
            StringEntity stringEntity = new StringEntity(data, ContentType.create(HTTP.PLAIN_TEXT_TYPE, Charsets.UTF_8));
            httpPost.setEntity(stringEntity);
        }
        httpClient.execute(httpPost, callback(completableFuture));
        return completableFuture;
    }


    /**
     * HttpClientUtil類回收時,通過httpClient.close(),可以間接的關閉連接池,從而關閉連接池持有的所有tcp連接
     * 與response.close()的區別:response.close()只是把某個請求持有的tcp連接放回連接池,而httpClient().close()是銷燬整個連接池
     *
     * @throws IOException
     */
    @Override
    public void close() throws IOException {
        httpClient.close();
    }


    public FutureCallback<HttpResponse> callback(CompletableFuture<String> completableFuture) {
        return new FutureCallback<HttpResponse>() {
            @Override
            public void completed(HttpResponse httpResponse) {
                try {
                    HttpEntity entity = httpResponse.getEntity();
                    if (entity == null) {
                        completableFuture.complete(null);
                        return;
                    }
                    String data = EntityUtils.toString(entity, Charsets.UTF_8);
                    completableFuture.complete(data);
                } catch (Exception ex) {
                    completableFuture.complete(null);
                    logger.error("excute get error ", ex);

                }
            }

            @Override
            public void failed(Exception ex) {
                completableFuture.obtrudeException(ex);
                logger.error("excute get error ", ex);
            }

            @Override
            public void cancelled() {
                completableFuture.cancel(false);
                logger.error("excute cancelled");
            }
        };
    }
}

同步工具類

package idslilang.com.just.go.async;

import org.apache.commons.codec.Charsets;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class HttpClientUtil implements AutoCloseable {

    private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);

    private final CloseableHttpClient httpClient;
    private final PoolingHttpClientConnectionManager connMgr;
    private final ThreadLocal<HttpClientContext> httpContext = ThreadLocal.withInitial(HttpClientContext::create);

    public HttpClientUtil() {

        // 自定義keep-alive策略,keep-alive使得tcp連接可以被複用。
        // 但默認的keep-alive時長爲無窮大,不符合現實,所以需要改寫,定義一個更短的時間
        // 如果服務器http響應頭包含 "Keep-Alive:timeout=" ,則使用timeout=後面指定的值作爲keep-alive的時長,否則默認60秒
        ConnectionKeepAliveStrategy strategy = (httpResponse, httpContext) -> {
            HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                String param = he.getName();
                String value = he.getValue();
                if (value != null && param.equalsIgnoreCase("timeout")) {
                    return Long.parseLong(value) * 1000;
                }
            }

            return 60 * 1000;

        };

        // 自定義連接池,連接池可以連接可以使連接可以被複用。
        // 連接的複用需要滿足:Keep-alive + 連接池。keep-alive使得連接能夠保持存活,不被系統銷燬;連接池使得連接可以被程序重複引用
        // 默認的連接池,DefaultMaxPerRoute只有5個,MaxTotal只有10個,不滿足實際的生產需求
        connMgr = new PoolingHttpClientConnectionManager();

        // 最大連接數500
        connMgr.setMaxTotal(500);
        // 同一個ip:port的請求,最大連接數200
        connMgr.setDefaultMaxPerRoute(200);

        // 定義請求超時配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(60)  // 從連接池裏獲取連接的超時時間
                .setConnectTimeout(60)            // 建立TCP的超時時間
                .setSocketTimeout(60)             // 讀取數據包的超時時間
                .build();

        httpClient = HttpClients.custom()
                .setConnectionManager(connMgr)
                .evictExpiredConnections()
                //開啓後臺線程清除閒置30秒以上的連接
                .evictIdleConnections(30, TimeUnit.SECONDS)
                .setKeepAliveStrategy(strategy)
                .setDefaultRequestConfig(requestConfig)
                .build();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                close();
            } catch (IOException e) {
                logger.info("graceful shutdown  fail");
            }
        }));


    }

    /**
     * 因爲HttpClient是線程安全的,可以提供給多個線程複用,同時連接池的存證的目的就是爲了可以複用連接,所以提供單例模式
     */
    private static class HttpClientUtilHolder {
        private static final HttpClientUtil INSTANCE = new HttpClientUtil();
    }

    public static HttpClientUtil getInstance() {
        return HttpClientUtilHolder.INSTANCE;
    }

    public PoolingHttpClientConnectionManager getConnMgr() {
        return connMgr;
    }

    public HttpContext getHttpContext() {
        return httpContext.get();
    }

    public CloseableHttpClient getHttpClient() {
        return httpClient;
    }

    /**
     * http get操作
     *
     * @param url     請求地址
     * @param headers 請求頭
     * @return 返回響應內容
     */
    public String get(String url, Map<String, String> headers) {
        HttpGet httpGet = new HttpGet(url);
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpGet.setHeader(header.getKey(), header.getValue());
            }
        }

        CloseableHttpResponse response = null;

        try {
            response = httpClient.execute(httpGet, httpContext.get());
            HttpEntity entity = response.getEntity();
            return EntityUtils.toString(entity, Charsets.UTF_8);
        } catch (Exception ex) {
            logger.error("excute get error ", ex);
        } finally {
            if (response != null) {
                try {
                    //上面的EntityUtils.toString會調用inputStream.close(),進而也會觸發連接釋放回連接池,但因爲httpClient.execute可能拋異常,所以得在finally顯示調一次,確保連接一定被釋放
                    response.close();
                } catch (IOException ex) {
                    logger.error("excute get error ", ex);
                }
            }
        }

        return null;
    }

    /**
     * form表單post
     *
     * @param url     請求地址
     * @param params  參數內容
     * @param headers http頭信息
     * @return http文本格式響應內容
     */
    public String post(String url, Map<String, String> params, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpPost.setHeader(header.getKey(), header.getValue());
            }
        }

        List<NameValuePair> formParams = new ArrayList<>();
        if (params != null) {
            for (Map.Entry<String, String> param : params.entrySet()) {
                formParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
            }
        }

        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(formParams, Charsets.UTF_8);
        httpPost.setEntity(formEntity);
        CloseableHttpResponse response = null;


        try {
            response = httpClient.execute(httpPost, httpContext.get());
            HttpEntity entity = response.getEntity();
            return EntityUtils.toString(entity, Charsets.UTF_8);
        } catch (Exception ex) {
            logger.error("excute postWithForm error ", ex);

        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException ex) {
                    logger.error("excute postWithForm error ", ex);
                }
            }
        }

        return null;
    }

    /**
     * json內容post
     *
     * @param url     請求地址
     * @param data    json報文
     * @param headers http頭
     * @return http文本格式響應內容
     */
    public String post(String url, String data, Map<String, String> headers) {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setHeader("Content-type", ContentType.APPLICATION_JSON.getMimeType());
        if (headers != null) {
            for (Map.Entry<String, String> header : headers.entrySet()) {
                httpPost.setHeader(header.getKey(), header.getValue());
            }
        }

        if (!StringUtils.isEmpty(data)) {
            StringEntity stringEntity = new StringEntity(data, ContentType.create(HTTP.PLAIN_TEXT_TYPE, Charsets.UTF_8));
            httpPost.setEntity(stringEntity);
        }
        CloseableHttpResponse response = null;

        try {
            response = httpClient.execute(httpPost, httpContext.get());
            HttpEntity entity = response.getEntity();
            return EntityUtils.toString(entity, Charsets.UTF_8);
        } catch (Exception ex) {
            logger.error("excute postWithBody error ", ex);
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException ex) {
                    logger.error("excute postWithBody error ", ex);
                }
            }
        }

        return null;
    }


    /**
     * HttpClientUtil類回收時,通過httpClient.close(),可以間接的關閉連接池,從而關閉連接池持有的所有tcp連接
     * 與response.close()的區別:response.close()只是把某個請求持有的tcp連接放回連接池,而httpClient().close()是銷燬整個連接池
     *
     * @throws IOException
     */
    @Override
    public void close() throws IOException {
        httpClient.close();
    }

}


Logback 配置:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<configuration>
    <property name="LOG_PATH" value="./logs"/>
    <property name="LOG_PATTEN" value="%date %-5level %logger{36} %line : %msg%n"/>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/idslilang.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/idslilang.log.%d{yyyyMMdd}.%i.gz</fileNamePattern>
            <maxFileSize>1GB</maxFileSize>
            <maxHistory>7</maxHistory>
            <totalSizeCap>120GB</totalSizeCap>
        </rollingPolicy>
        <encoder charset="UTF-8">
            <pattern>${LOG_PATTEN}</pattern>
        </encoder>
    </appender>

    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/err.idslilang.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/err.idslilang.log.%d{yyyyMMdd}.%i.gz</fileNamePattern>
            <maxFileSize>1GB</maxFileSize>
            <maxHistory>7</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <encoder charset="UTF-8">
            <pattern>${LOG_PATTEN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <OnMismatch>DENY</OnMismatch>
            <OnMatch>ACCEPT</OnMatch>
        </filter>
    </appender>
    <root level="INFO">
        <appender-ref ref="FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>

</configuration>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章