1. 程式人生 > 實用技巧 >【工具】- HttpClient篇

【工具】- HttpClient篇

簡介

  • 對於httpclient,相信很多人或多或少接觸過,對於httpclient的使用姿勢,相信很多人會有疑問?下面這邊會通過程式碼說明
package xxx;

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.conn.HttpClientConnectionManager;
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 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 volatile static HttpClientUtil httpClientUtil;
    private final CloseableHttpClient httpClient;
    private final PoolingHttpClientConnectionManager connMgr;
    private final ThreadLocal<HttpClientContext> httpContext = ThreadLocal.withInitial(HttpClientContext::create);
    private final IdleConnectionEvictThread evictThread;

    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);

        // 啟動執行緒池空閒連線、超時連線監控執行緒
        evictThread = new IdleConnectionEvictThread(connMgr);
        evictThread.start();

        // 定義請求超時配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(5000)  // 從連線池裡獲取連線的超時時間
                .setConnectTimeout(2000)            // 建立TCP的超時時間
                .setSocketTimeout(10000)             // 讀取資料包的超時時間
                .build();


        httpClient = HttpClients.custom()
                .setConnectionManager(connMgr)
                .setKeepAliveStrategy(strategy)
                .setDefaultRequestConfig(requestConfig)
                .build();

    }

    /**
     * 因為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) {
            ex.printStackTrace();
        } finally {
            if (response != null) {
                try {
                    //上面的EntityUtils.toString會呼叫inputStream.close(),進而也會觸發連線釋放回連線池,但因為httpClient.execute可能拋異常,所以得在finally顯示調一次,確保連線一定被釋放
                    response.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }

        return null;
    }

    /**
     * form表單post
     *
     * @param url     請求地址
     * @param params  引數內容
     * @param headers http頭資訊
     * @return http文字格式響應內容
     */
    public String postWithForm(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) {
            ex.printStackTrace();
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }

        return null;
    }

    /**
     * json內容post
     *
     * @param url     請求地址
     * @param data    json報文
     * @param headers http頭
     * @return http文字格式響應內容
     */
    public String postWithBody(String url, String data, 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());
            }
        }

        StringEntity stringEntity = new StringEntity(data, ContentType.create("plain/text", Charsets.UTF_8));
        httpPost.setEntity(stringEntity);
        httpPost.setHeader("Content-type", "application/json");
        CloseableHttpResponse response = null;

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

        return null;
    }

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

    /**
     * 定義一個連線監控執行緒類,用以從連線池裡關閉過期的連線(即伺服器已經關閉的連線),以及在30秒內處於空閒的連線;每5秒鐘處理一次
     */
    static class IdleConnectionEvictThread extends Thread {
        private final HttpClientConnectionManager connMgr;
        private volatile boolean shutdown;

        public IdleConnectionEvictThread(HttpClientConnectionManager connMgr) {
            super();
            this.connMgr = connMgr;
            setDaemon(true);
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(5000);
                        connMgr.closeExpiredConnections();
                        connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                    }
                }
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }

        /**
         * 關閉監控執行緒
         */
        public void shutdown() {
            shutdown = true;
            // 監控執行緒可能還處於wait()狀態,通過notifyAll()喚醒,及時退出while迴圈
            synchronized (this) {
                notifyAll();
            }
        }
    }
}

參考書籍:httpclient-tutorial.pdf