1. 程式人生 > >輕量級簡易 Java http 網路請求的封裝: SimpleHttpUtils

輕量級簡易 Java http 網路請求的封裝: SimpleHttpUtils

1. 概述

筆者有時想臨時寫個小Demo或小工具,或者想爬一個網頁連結,需要使用 HTTP 請求,那就先要對 HttpURLConnection 進行簡單的封裝,或者找個成熟的 Java 網路庫,也要進行小小的封裝。想起這些前期與主要業務邏輯無關的基礎工作,可能興趣已經不大或不太想動手了。等這些基礎工作較完善的做完後,已經花去了不少時間,這也是 Java 開發比較繁瑣的地方,很多東西都要封裝,所以平時可以積累寫一些較完善的、無其他依賴的、儘可能簡單實用的工具類,需要用時拷貝即用。

下面我就封裝一個簡單的 HTTP 請求工具,所有的邏輯都在一個類中,不需要任何設定,使用一個靜態方法就完成完整的一個 HTTP 請求,並支援自動處理重定向,自動識別 charset,支援 Cookie 的自動儲存與傳送,我把它稱之為 SimpleHttpUtils

2. 介面說明

  • 全域性設定
// (可選) 設定預設的User-Agent是否為移動瀏覽器模式, 預設為PC瀏覽器模式
public static void setMobileBrowserModel(boolean isMobileBrowser);

// (可選) 設定預設的請求頭, 每次請求時都將會 新增 並 覆蓋 原有的預設請求頭
public static void setDefaultRequestHeader(String key, String value);

// (可選) 設定 連線 和 讀取 的超時時間, 連線超時時間預設為15000毫秒, 讀取超時時間為0(即不檢查超時)
public
static void setTimeOut(int connectTimeOut, int readTimeOut);
  • GET 請求
// 返回響應文字
public static String get(String url);
public static String get(String url, Map<String, String> headers);

// 下載檔案, 返回檔案路徑
public static String get(String url, File saveToFile);
public static String get(String url, Map<
String, String>
headers, File saveToFile);
  • POST 請求
// 提交資料, 返回響應文字
public static String post(String url, byte[] body);
public static String post(String url, Map<String, String> headers, byte[] body);

// 上傳檔案, 返回響應文字
public static String post(String url, File bodyFile);
public static String post(String url, Map<String, String> headers, File bodyFile);

// 從輸入流中讀取資料上傳, 返回響應文字
public static String post(String url, InputStream bodyStream);
public static String post(String url, Map<String, String> headers, InputStream bodyStream);
  • 通用的 HTTP / HTTPS 請求
/*
 * 每一個引數的說明詳見最後的 SimpleHttpUtils 類中的程式碼,
 * 該介面繁瑣,建設直接使用上面說明的 get(...) 和 post(...) 方法
 */
public static String sendRequest(String url, 
                                 String method, 
                                 Map<String, String> headers, 
                                 InputStream bodyStream, 
                                 File saveToFile);

3. SimpleHttpUtils 類完整封裝, 以及簡單使用案例

案例主函式入口:

package com.xiets.http;

public class Main {

    public static void main(String[] args) throws Exception {
        // GET 請求, 返回響應文字
        String html = SimpleHttpUtils.get("http://blog.csdn.net/");

        System.out.println(html);
    }

}

對 HttpURLConnection 的封裝,檔案: SimpleHttpUtils.java

package com.xiets.http;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 簡易的 HTTP 請求工具, 一個靜態方法完成請求, 支援 301, 302 的重定向, 支援自動識別 charset, 支援同進程中 Cookie 的自動儲存與傳送  <br/><br/>
 *
 * Demo:
 *
 * <pre>{@code
 *     // (可選) 設定預設的User-Agent是否為移動瀏覽器模式, 預設為false(PC瀏覽器模式)
 *     SimpleHttpUtils.setMobileBrowserModel(true);
 *     // (可選) 設定預設的請求頭, 每次請求時都將會 新增 並 覆蓋 原有的預設請求頭
 *     SimpleHttpUtils.setDefaultRequestHeader("header-key", "header-value");
 *     // (可選) 設定 連線 和 讀取 的超時時間, 連線超時時間預設為15000毫秒, 讀取超時時間為0(即不檢查超時)
 *     SimpleHttpUtils.setTimeOut(15000, 0);
 *
 *     // GET 請求, 返回響應文字
 *     String html = SimpleHttpUtils.get("http://blog.csdn.net/");
 *
 *     // GET 請求, 下載檔案, 返回檔案路徑
 *     SimpleHttpUtils.get("http://blog.csdn.net/", new File("csdn.txt"));
 *
 *     // POST 請求, 返回響應文字
 *     String respText = SimpleHttpUtils.post("http://url", "body-data".getBytes());
 *
 *     // POST 請求, 上傳檔案, 返回響應文字
 *     SimpleHttpUtils.post("http://url", new File("aa.jpg"));
 *
 *     // 還有其他若干 get(...) 和 post(...) 方法的過載(例如請求時單獨新增請求頭), 詳見程式碼實現
 * }</pre>
 *
 * @author xietansheng
 */
public class SimpleHttpUtils {

    /** 文字請求時所限制的最大響應長度, 5MB */
    private static final int TEXT_REQUEST_MAX_LENGTH = 5 * 1024 * 1024;

    /** 預設的請求頭 */
    private static final Map<String, String> DEFAULT_REQUEST_HEADERS = new HashMap<String, String>();

    /** 操作預設請求頭的讀寫鎖 */
    private static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock();

    /** User-Agent PC: Windows10 IE 11 */
    private static final String USER_AGENT_FOR_PC = "Mozilla 0.0 Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko";

    /** User-Agent Mobile: Android 7.0 Chrome 瀏覽器 */
    private static final String USER_AGENT_FOR_MOBILE = "Chrome Mozilla/5.0 (Linux; Android 7.0; Nexus 6 Build/NBD92D) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.132 Mobile Safari/537.36";

    /** 連線超時時間, 單位: ms, 0 表示無窮大超時(即不檢查超時), 預設 15s 超時 */
    private static int CONNECT_TIME_OUT = 15 * 1000;

    /** 讀取超時時間, 單位: ms, 0 表示無窮大超時(即不檢查超時), 預設為 0 */
    private static int READ_TIME_OUT = 0;

    static {
        // 設定一個預設的 Cookie 管理器, 使用 HttpURLConnection 請求時,
        // 會在記憶體中自動儲存相應的 Cookie, 並且在下一個請求時自動傳送相應的 Cookie
        CookieHandler.setDefault(new CookieManager());

        // 預設使用PC瀏覽器模式
        setMobileBrowserModel(false);
    }

    /**
     * 設定預設的User-Agent是否為移動瀏覽器模式, 預設為PC瀏覽器模式,  <br/>
     *
     * 也可以通過 {@link #setDefaultRequestHeader(String, String)} 自定義設定 User-Agent
     *
     * @param isMobileBrowser true: 手機瀏覽器; false: PC瀏覽器
     */
    public static void setMobileBrowserModel(boolean isMobileBrowser) {
        setDefaultRequestHeader("User-Agent", isMobileBrowser ? USER_AGENT_FOR_MOBILE : USER_AGENT_FOR_PC);
    }

    /**
     * 設定超時時間, 單位: ms, 0 表示無窮大超時(即不檢查超時)
     *
     * @param connectTimeOut 連線超時時間, 預設為 15s
     * @param readTimeOut 讀取超時時間, 預設為 0
     */
    public static void setTimeOut(int connectTimeOut, int readTimeOut) {
        if (connectTimeOut < 0 || readTimeOut < 0) {
            throw new IllegalArgumentException("timeout can not be negative");
        }
        RW_LOCK.writeLock().lock();
        try {
            CONNECT_TIME_OUT = connectTimeOut;
            READ_TIME_OUT = readTimeOut;
        } finally {
            RW_LOCK.writeLock().unlock();
        }
    }

    /**
     * 設定預設的請求頭, 每次請求時都將會 新增 並 覆蓋 原有的預設請求頭
     */
    public static void setDefaultRequestHeader(String key, String value) {
        RW_LOCK.writeLock().lock();
        try {
            DEFAULT_REQUEST_HEADERS.put(key, value);
        } finally {
            RW_LOCK.writeLock().unlock();
        }
    }

    /**
     * 移除預設的請求頭
     */
    public static void removeDefaultRequestHeader(String key) {
        RW_LOCK.writeLock().lock();
        try {
            DEFAULT_REQUEST_HEADERS.remove(key);
        } finally {
            RW_LOCK.writeLock().unlock();
        }
    }

    public static String get(String url) throws Exception  {
        return get(url, null, null);
    }

    public static String get(String url, Map<String, String> headers) throws Exception  {
        return get(url, headers, null);
    }

    public static String get(String url, File saveToFile) throws Exception  {
        return get(url, null, saveToFile);
    }

    public static String get(String url, Map<String, String> headers, File saveToFile) throws Exception  {
        return sendRequest(url, "GET", headers, null, saveToFile);
    }

    public static String post(String url, byte[] body) throws Exception {
        return post(url, null, body);
    }

    public static String post(String url, Map<String, String> headers, byte[] body) throws Exception {
        InputStream in = null;
        if (body != null && body.length > 0) {
            in = new ByteArrayInputStream(body);
        }
        return post(url, headers, in);
    }

    public static String post(String url, File bodyFile) throws Exception {
        return post(url, null, bodyFile);
    }

    public static String post(String url, Map<String, String> headers, File bodyFile) throws Exception {
        InputStream in = null;
        if (bodyFile != null && bodyFile.exists() && bodyFile.isFile() && bodyFile.length() > 0) {
            in = new FileInputStream(bodyFile);
        }
        return post(url, headers, in);
    }

    public static String post(String url, InputStream bodyStream) throws Exception {
        return post(url, null, bodyStream);
    }

    public static String post(String url, Map<String, String> headers, InputStream bodyStream) throws Exception {
        return sendRequest(url, "POST", headers, bodyStream, null);
    }

    /**
     * 執行一個通用的 http/https 請求, 支援 301, 302 的重定向, 支援自動識別 charset, 支援同進程中 Cookie 的自動儲存與傳送
     *
     * @param url
     *          請求的連結, 只支援 http 和 https 連結
     *
     * @param method
     *          (可選) 請求方法, 可以為 null
     *
     * @param headers
     *          (可選) 請求頭 (將覆蓋預設請求), 可以為 null
     *
     * @param bodyStream
     *          (可選) 請求內容, 流將自動關閉, 可以為 null
     *
     * @param saveToFile
     *          (可選) 響應儲存到該檔案, 可以為 null
     *
     * @return
     *          如果響應內容儲存到檔案, 則返回檔案路徑, 否則返回響應內容的文字 (自動解析 charset 進行解碼)
     *
     * @throws Exception
     *          http 響應 code 非 200, 或發生其他異常均丟擲異常
     */
    public static String sendRequest(String url, String method, Map<String, String> headers, InputStream bodyStream, File saveToFile) throws Exception {
        assertUrlValid(url);

        HttpURLConnection conn = null;

        try {
            // 開啟連結
            URL urlObj = new URL(url);
            conn = (HttpURLConnection) urlObj.openConnection();

            // 設定各種預設屬性
            setDefaultProperties(conn);

            // 設定請求方法
            if (method != null && method.length() > 0) {
                conn.setRequestMethod(method);
            }

            // 新增請求頭
            if (headers != null && headers.size() > 0) {
                for (Map.Entry<String, String> entry : headers.entrySet()) {
                    conn.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            // 設定請求內容
            if (bodyStream != null) {
                conn.setDoOutput(true);
                copyStreamAndClose(bodyStream, conn.getOutputStream());
            }

            // 獲取響應code
            int code = conn.getResponseCode();

            // 處理重定向
            if (code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_MOVED_TEMP) {
                String location = conn.getHeaderField("Location");
                if (location != null) {
                    closeStream(bodyStream);
                    // 重定向為 GET 請求
                    return sendRequest(location, "GET", headers, null, saveToFile);
                }
            }

            // 獲取響應內容長度
            long contentLength = conn.getContentLengthLong();
            // 獲取內容型別
            String contentType = conn.getContentType();

            // 獲取響應內容輸入流
            InputStream in = conn.getInputStream();

            // 沒有響應成功, 均丟擲異常
            if (code != HttpURLConnection.HTTP_OK) {
                throw new IOException("Http Error: " + code + "; Desc: " + handleResponseBodyToString(in, contentType));
            }

            // 如果檔案引數不為null, 則把響應內容儲存到檔案
            if (saveToFile != null) {
                handleResponseBodyToFile(in, saveToFile);
                return saveToFile.getPath();
            }

            // 如果需要將響應內容解析為文字, 則限制最大長度
            if (contentLength > TEXT_REQUEST_MAX_LENGTH) {
                throw new IOException("Response content length too large: " + contentLength);
            }
            return handleResponseBodyToString(in, contentType);

        } finally {
            closeConnection(conn);
        }
    }

    private static void assertUrlValid(String url) throws IllegalAccessException {
        boolean isValid = false;
        if (url != null) {
            url = url.toLowerCase();
            if (url.startsWith("http://") || url.startsWith("https://")) {
                isValid = true;
            }
        }
        if (!isValid) {
            throw new IllegalAccessException("Only support http or https url: " + url);
        }
    }

    private static void setDefaultProperties(HttpURLConnection conn) {
        RW_LOCK.readLock().lock();
        try {
            // 設定連線超時時間
            conn.setConnectTimeout(CONNECT_TIME_OUT);

            // 設定讀取超時時間
            conn.setReadTimeout(READ_TIME_OUT);

            // 新增預設的請求頭
            if (DEFAULT_REQUEST_HEADERS.size() > 0) {
                for (Map.Entry<String, String> entry : DEFAULT_REQUEST_HEADERS.entrySet()) {
                    conn.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }
        } finally {
            RW_LOCK.readLock().unlock();
        }
    }

    private static void handleResponseBodyToFile(InputStream in, File saveToFile) throws Exception  {
        OutputStream out = null;
        try {
            out = new FileOutputStream(saveToFile);
            copyStreamAndClose(in, out);
        } finally {
            closeStream(out);
        }
    }

    private static String handleResponseBodyToString(InputStream in, String contentType) throws Exception {
        ByteArrayOutputStream bytesOut = null;

        try {
            bytesOut = new ByteArrayOutputStream();

            // 讀取響應內容
            copyStreamAndClose(in, bytesOut);

            // 響應內容的位元組序列
            byte[] contentBytes = bytesOut.toByteArray();

            // 解析文字內容編碼格式
            String charset = parseCharset(contentType);
            if (charset == null) {
                charset = parseCharsetFromHtml(contentBytes);
                if (charset == null) {
                    charset = "utf-8";
                }
            }

            // 解碼響應內容
            String content = null;
            try {
                content = new String(contentBytes, charset);
            }