Apache HttpClient 5 使用詳細教程

HTTP Components Logo

超文本傳輸協議(HTTP)可能是當今互聯網上最重要的協議之一,Web 服務、微服務以及支持網絡的各種設備上的服務幾乎都是 HTTP 協議,HTTP 協議已經從 Web 瀏覽器走向了更廣泛的使用場景。

雖然 java.net 包已經提供了 HTTP 訪問資源的基本功能,但是它不夠靈活,而且不能隨心所欲的進行自定義。Apache HttpClient 5 是一個開源的 HTTP 工具包,可以支持最新 HTTP 協議標準,且有豐富的 API 和強大的擴展特性,可以用於構建任何需要進行 HTTP 協議處理的應用程序。

這篇文章介紹 Apache HttpClient 5 中最爲常見的一些用法,通過這篇文章可以快速的入門使用 HttpClient 5,主要內容包括 HttpClient 5 的 Get 請求、Post 請求、如何攜帶參數、JSON 參數、設置超時、異步請求、操作 Cookie、表單登錄、基本認證、Digest 認證以及自定義 HTTP 請求攔截器等。

HttpClient 5 依賴

HttpClient 5 Maven 依賴

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5-fluent -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5-fluent</artifactId>
    <version>5.1.3</version>
</dependency>

HttpClient 5 Gradle 依賴

implementation 'org.apache.httpcomponents.client5:httpclient5:5.1.3'
implementation 'org.apache.httpcomponents.client5:httpclient5-fluent:5.1.3'

HttpClient 5 GET 請求

package com.wdbyte.httpclient;

import java.io.IOException;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;

/**
 * @author https://www.wdbyte.com
 */
public class HttpClient5Get {

    public static void main(String[] args) {
        String result = get("http://httpbin.org/get");
        System.out.println(result);
    }

    public static String get(String url) {
        String resultContent = null;
        HttpGet httpGet = new HttpGet(url);
        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
                // 獲取狀態碼
                System.out.println(response.getVersion()); // HTTP/1.1
                System.out.println(response.getCode()); // 200
                System.out.println(response.getReasonPhrase()); // OK
                HttpEntity entity = response.getEntity();
                // 獲取響應信息
                resultContent = EntityUtils.toString(entity);
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return resultContent;
    }

}

響應信息:

HTTP/1.1
200
OK
{
  "args"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb1891-5ab5e5376ed960471bf32f17"
  }, 
  "origin""47.251.4.198", 
  "url""http://httpbin.org/get"
}

HttpClient 5 Fluent GET

使用 Apache HttpClient 5 提供的 Fluent API 可以更便捷的發起 GET 請求,但是可操作的地方較少。

依賴:

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5-fluent -->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5-fluent</artifactId>
    <version>5.1.3</version>
</dependency>

示例:

package com.wdbyte.httpclient;

import java.io.IOException;

import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.client5.http.fluent.Response;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5GetFluent {

    public static void main(String[] args) {
        System.out.println(get("http://httpbin.org/get"));
    }

    public static String get(String url) {
        String result = null;
        try {
            Response response = Request.get(url).execute();
            result = response.returnContent().asString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

}

輸出信息:

{
  "args"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb190e-1ba46a92645843a04c55da32"
  }, 
  "origin""47.251.4.198", 
  "url""http://httpbin.org/get"
}

HttpClient5 GET 請求參數

使用 URIBuilder 的 addParameters() 方法來構建 GET 請求的參數。

package com.wdbyte.httpclient;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.apache.hc.core5.net.URIBuilder;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5GetParams {

    public static void main(String[] args) {
        String result = get("http://httpbin.org/get");
        System.out.println(result);
    }

    public static String get(String url) {
        String resultContent = null;
        HttpGet httpGet = new HttpGet(url);
        // 表單參數
        List<NameValuePair> nvps = new ArrayList<>();
        // GET 請求參數
        nvps.add(new BasicNameValuePair("username""wdbyte.com"));
        nvps.add(new BasicNameValuePair("password""secret"));
        // 增加到請求 URL 中
        try {
            URI uri = new URIBuilder(new URI(url))
                .addParameters(nvps)
                .build();
            httpGet.setUri(uri);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }

        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
                // 獲取狀態碼
                System.out.println(response.getVersion()); // HTTP/1.1
                System.out.println(response.getCode()); // 200
                System.out.println(response.getReasonPhrase()); // OK
                HttpEntity entity = response.getEntity();
                // 獲取響應信息
                resultContent = EntityUtils.toString(entity);
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return resultContent;
    }
}

輸出信息:

{
  "args"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/1.8.0_151)", 
    "X-Amzn-Trace-Id""Root=1-62ecc660-69d58a226aefb1b6226541ec"
  }, 
  "origin""42.120.75.185", 
  "url""http://httpbin.org/get?user
}

下面是通過抓包得到的請求響應信息格式:

// 請求信息
GET /get?username=wdbyte.com&password=secret HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)

// 響應信息
HTTP/1.1 200 OK
Date: Fri, 05 Aug 2022 07:27:30 GMT
Content-Type: application/json
Content-Length: 405
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/1.8.0_151)", 
    "X-Amzn-Trace-Id""Root=1-62ecc660-69d58a226aefb1b6226541ec"
  }, 
  "origin""42.120.75.185", 
  "url""http://httpbin.org/get?user
}

HttpClient 5 POST 請求

下面演示發起一個 POST 請求,並攜帶表單參數。

參數:username=wdbyte.com&password=secret

package com.wdbyte.httpclient;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5Post {

    public static void main(String[] args) {
        String result = post("http://httpbin.org/post");
        System.out.println(result);
    }
    public static String post(String url) {
        String result = null;
        HttpPost httpPost = new HttpPost(url);
        // 表單參數
        List<NameValuePair> nvps = new ArrayList<>();
        // POST 請求參數
        nvps.add(new BasicNameValuePair("username""wdbyte.com"));
        nvps.add(new BasicNameValuePair("password""secret"));
        httpPost.setEntity(new UrlEncodedFormEntity(nvps));
        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
                System.out.println(response.getVersion()); // HTTP/1.1
                System.out.println(response.getCode()); // 200
                System.out.println(response.getReasonPhrase()); // OK

                HttpEntity entity = response.getEntity();
                // 獲取響應信息
                result = EntityUtils.toString(entity);
                // 確保流被完全消費
                EntityUtils.consume(entity);
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return result;
    }

}

輸出信息:

HTTP/1.1
200
OK
{
  "args"{}, 
  "data""", 
  "files"{}, 
  "form"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Content-Length""35", 
    "Content-Type""application/x-www-form-urlencoded; charset=ISO-8859-1", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb1ac8-489b2100728c81d70797a482"
  }, 
  "json": null, 
  "origin""183.128.136.89", 
  "url""http://httpbin.org/post"
}

下面是通過 Wireshark 抓包得到的請求信息:

POST /post HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Content-Length: 35
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/17)

username=wdbyte.com&password=secret

HttpClient 5 Fluent POST

使用 Apache HttpClient 5 提供的 Fluent API 可以更便捷的發起 POST 請求,但是可操作的地方較少。

一樣發送一個簡單的表單參數:username=wdbyte.com&password=secret

package com.wdbyte.httpclient;

import java.io.IOException;

import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.http.message.BasicNameValuePair;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5PostFluent {

    public static void main(String[] args) {
        String result = post("http://httpbin.org/post");
        System.out.println(result);
    }

    public static String post(String url) {
        String result = null;
        Request request = Request.post(url);
        // POST 請求參數
        request.bodyForm(
            new BasicNameValuePair("username""wdbyte.com"),
            new BasicNameValuePair("password""secret"));
        try {
            result = request.execute().returnContent().asString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}

輸出信息:

{
  "args"{}, 
  "data""", 
  "files"{}, 
  "form"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Content-Length""35", 
    "Content-Type""application/x-www-form-urlencoded; charset=ISO-8859-1", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb1c8a-7aee8c004f06919f31a2b533"
  }, 
  "json": null, 
  "origin""183.128.136.89", 
  "url""http://httpbin.org/post"
}

HttpClient5 POST JSON 參數

使用 StringEntity 類存入 JSON 參數。

package com.wdbyte.httpclient;

import java.io.IOException;

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5PostWithJson {

    public static void main(String[] args) {
        String json = "{"
            + "    \"password\": \"secret\","
            + "    \"username\": \"wdbyte.com\""
            + "}";
        String result = post("http://httpbin.org/post", json);
        System.out.println(result);
    }

    public static String post(String url, String jsonBody) {
        String result = null;
        HttpPost httpPost = new HttpPost(url);
        httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
      
        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
                // 獲取響應信息
                result = EntityUtils.toString(response.getEntity());
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return result;
    }

}

輸出信息:

{
  "args"{}, 
  "data""{    \"password\": \"secret\",    \"username\": \"wdbyte.com\"}", 
  "files"{}, 
  "form"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Content-Length""55", 
    "Content-Type""text/plain; charset=ISO-8859-1", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb1dbb-5a963c1d798b06be3ee1a15e"
  }, 
  "json"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "origin""183.128.136.89", 
  "url""http://httpbin.org/post"
}

下面是通過 Wireshark 抓包得到的請求響應信息:

// 請求信息
POST /post HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Content-Length: 55
Content-Type: application/json; charset=UTF-8
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/17)

{    "password""secret",    "username""wdbyte.com"}

// 響應信息
HTTP/1.1 200 OK
Date: Tue, 28 Jun 2022 15:30:17 GMT
Content-Type: application/json
Content-Length: 573
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "args"{}, 
  "data""{    \"password\": \"secret\",    \"username\": \"wdbyte.com\"}", 
  "files"{}, 
  "form"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Content-Length""55", 
    "Content-Type""application/json; charset=UTF-8", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/17)", 
    "X-Amzn-Trace-Id""Root=1-62bb1e89-64db55730a0361c720232ccd"
  }, 
  "json"{
    "password""secret", 
    "username""wdbyte.com"
  }, 
  "origin""183.128.136.89", 
  "url""http://httpbin.org/post"
}

HttpClient 5 設置超時

使用 RequestConfig 對象來配置超時時間。

package com.wdbyte.httpclient;

import java.io.IOException;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.util.Timeout;

/**
* @author https://www.wdbyte.com
 */
public class HttpClient5GetWithTimeout {

    public static void main(String[] args) {
        String result = get("http://httpbin.org/get");
        System.out.println(result);
    }

    public static String get(String url) {
        String resultContent = null;
        // 設置超時時間
        RequestConfig config = RequestConfig.custom()
            .setConnectTimeout(Timeout.ofMilliseconds(5000L))
            .setConnectionRequestTimeout(Timeout.ofMilliseconds(5000L))
            .setResponseTimeout(Timeout.ofMilliseconds(5000L))
            .build();
        // 請求級別的超時
        HttpGet httpGet = new HttpGet(url);
        //httpGet.setConfig(config);
        //try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
        // 客戶端級別的超時
        try (CloseableHttpClient httpclient = HttpClients.custom().setDefaultRequestConfig(config).build()) {
            try (CloseableHttpResponse response = httpclient.execute(httpGet)) {
                // 獲取狀態碼
                System.out.println(response.getVersion()); // HTTP/1.1
                System.out.println(response.getCode()); // 200
                System.out.println(response.getReasonPhrase()); // OK
                HttpEntity entity = response.getEntity();
                // 獲取響應信息
                resultContent = EntityUtils.toString(entity);
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
        }
        return resultContent;
    }

}

HttpClient 5 異步請求

下面演示三種 HttpClient 5 異步請求方式。

package com.wdbyte.httpclient;

import java.io.IOException;
import java.nio.CharBuffer;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.hc.client5.http.async.methods.AbstractCharResponseConsumer;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
import org.apache.hc.client5.http.async.methods.SimpleHttpRequests;
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.nio.AsyncRequestProducer;
import org.apache.hc.core5.http.nio.support.AsyncRequestBuilder;

/**
 * HttpClient 5 異步請求
* @author https://www.wdbyte.com
 * @date 2022/06/25
 */
public class HttpClient5Async {

    public static void main(String[] args) {
        getAsync1("http://httpbin.org/get");
        getAsync2("http://httpbin.org/get");
        getAsync3("http://httpbin.org/get");
    }

    /**
     * 異步請求
     *
     * @param url
     * @return
     */
    public static String getAsync1(String url) {
        try (CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault()) {
            // 開始 http clinet
            httpclient.start();
            // 執行請求
            SimpleHttpRequest request1 = SimpleHttpRequests.get(url);
            Future<SimpleHttpResponse> future = httpclient.execute(request1, null);
            // 等待直到返回完畢
            SimpleHttpResponse response1 = future.get();
            System.out.println("getAsync1:" + request1.getRequestUri() + "->" + response1.getCode());
        } catch (IOException | ExecutionException | InterruptedException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    /**
     * 異步請求,根據響應情況回調
     *
     * @param url
     * @return
     */
    public static String getAsync2(String url) {
        try (CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault()) {
            // 開始 http clinet
            httpclient.start();
            // 根據請求響應情況進行回調操作
            CountDownLatch latch = new CountDownLatch(1);
            SimpleHttpRequest request = SimpleHttpRequests.get(url);
            httpclient.execute(request, new FutureCallback<SimpleHttpResponse>() {
                @Override
                public void completed(SimpleHttpResponse response2) {
                    latch.countDown();
                    System.out.println("getAsync2:" + request.getRequestUri() + "->" + response2.getCode());
                }

                @Override
                public void failed(Exception ex) {
                    latch.countDown();
                    System.out.println("getAsync2:" + request.getRequestUri() + "->" + ex);
                }

                @Override
                public void cancelled() {
                    latch.countDown();
                    System.out.println("getAsync2:" + request.getRequestUri() + " cancelled");
                }

            });
            latch.await();
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
        return null;
    }

    /**
     * 異步請求,對響應流做點什麼
     *
     * @param url
     * @return
     */
    public static String getAsync3(String url) {
        try (CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault()) {
            // 開始 http clinet
            httpclient.start();
            // 根據請求響應情況進行回調操作
            SimpleHttpRequest request = SimpleHttpRequests.get(url);

            CountDownLatch latch = new CountDownLatch(1);
            AsyncRequestProducer producer = AsyncRequestBuilder.get("http://httpbin.org/get").build();
            AbstractCharResponseConsumer<HttpResponse> consumer3 = new AbstractCharResponseConsumer<HttpResponse>() {

                HttpResponse response;

                @Override
                protected void start(HttpResponse response, ContentType contentType) throws HttpException, IOException {
                    System.out.println("getAsync3: 開始響應....");
                    this.response = response;
                }

                @Override
                protected int capacityIncrement() {
                    return Integer.MAX_VALUE;
                }

                @Override
                protected void data(CharBuffer data, boolean endOfStream) throws IOException {
                    System.out.println("getAsync3: 收到數據....");
                    // Do something useful
                }

                @Override
                protected HttpResponse buildResult() throws IOException {
                    System.out.println("getAsync3: 接收完畢...");
                    return response;
                }

                @Override
                public void releaseResources() {
                }

            };
            httpclient.execute(producer, consumer3, new FutureCallback<HttpResponse>() {

                @Override
                public void completed(HttpResponse response) {
                    latch.countDown();
                    System.out.println("getAsync3: "+request.getRequestUri() + "->" + response.getCode());
                }

                @Override
                public void failed(Exception ex) {
                    latch.countDown();
                    System.out.println("getAsync3: "+request.getRequestUri() + "->" + ex);
                }

                @Override
                public void cancelled() {
                    latch.countDown();
                    System.out.println("getAsync3: "+request.getRequestUri() + " cancelled");
                }

            });
            latch.await();
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
        return null;

    }
}

輸出結果:

getAsync1:/get->200
getAsync2:/get->200
getAsync3: 開始響應....
getAsync3: 收到數據....
getAsync3: 收到數據....
getAsync3: 收到數據....
getAsync3: 接收完畢...
getAsync3: /get->200

請求 http://httpbin.org/cookies/set/cookieName/www.wdbyte.com 的響應中會帶有一個 Cookie 信息,其中 name 爲 cookieName,value 爲 www.wdbyte.com,我們以此用作測試。

Postman 請求測試,可以看到響應了 Cookie 信息。

下面編寫 Java 代碼進行請求測試

package com.wdbyte.httpclient;

import java.util.List;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.cookie.CookieStore;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.cookie.BasicClientCookie;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.io.entity.EntityUtils;

/**
 * 這個例子演示了使用本地HTTP上下文填充, 自定義屬性
 */
public class HttpClient5WithCookie {

    public static void main(final String[] args) throws Exception {
        try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
            // 創建一個本地的 Cookie 存儲
            final CookieStore cookieStore = new BasicCookieStore();
            // BasicClientCookie clientCookie = new BasicClientCookie("name""www.wdbyte.com");
            // clientCookie.setDomain("http://httpbin.org/cookies");
            // 過期時間
            // clientCookie.setExpiryDate(new Date());
            // 添加到本地 Cookie
            // cookieStore.addCookie(clientCookie);

            // 創建本地 HTTP 請求上下文 HttpClientContext
            final HttpClientContext localContext = HttpClientContext.create();
            // 綁定 cookieStore 到 localContext
            localContext.setCookieStore(cookieStore);

            final HttpGet httpget = new HttpGet("http://httpbin.org/cookies/set/cookieName/www.wdbyte.com");
            System.out.println("執行請求 " + httpget.getMethod() + " " + httpget.getUri());

            // 獲取 Coolie 信息
            try (final CloseableHttpResponse response = httpclient.execute(httpget, localContext)) {
                System.out.println("----------------------------------------");
                System.out.println(response.getCode() + " " + response.getReasonPhrase());
                final List<Cookie> cookies = cookieStore.getCookies();
                for (int i = 0; i < cookies.size(); i++) {
                    System.out.println("Local cookie: " + cookies.get(i));
                }
                EntityUtils.consume(response.getEntity());
            }
        }
    }

}

輸出結果:

執行請求 GET http://httpbin.org/cookies/set/cookieName/www.wdbyte.com
----------------------------------------
200 OK
Local cookie: [name: cookieName; value: www.wdbyte.com; domain: httpbin.org; path: /; expiry: null]

HttpClient 5 讀取文件內容請求

準備一個 JSON 內容格式的文件 params.json。

{"name":"www.wdbyte.com"}

讀取這個文件作爲請求參數發起請求。

package com.wdbyte.httpclient;

import java.io.File;
import java.io.FileInputStream;

import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.FileEntity;
import org.apache.hc.core5.http.io.entity.InputStreamEntity;

/**
 * 加載數據流作爲 POST 請求參數
 */
public class HttpClient5ChunkEncodedPost {

    public static void main(final String[] args) throws Exception {
        String params = "/Users/darcy/params.json";

        try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {
            final HttpPost httppost = new HttpPost("http://httpbin.org/post");

            final InputStreamEntity reqEntity = new InputStreamEntity(new FileInputStream(params), -1,
                ContentType.APPLICATION_JSON);
            // 也可以使用 FileEntity 的形式
            // FileEntity reqEntity = new FileEntity(new File(params), ContentType.APPLICATION_JSON);

            httppost.setEntity(reqEntity);

            System.out.println("執行請求 " + httppost.getMethod() + " " + httppost.getUri());
            try (final CloseableHttpResponse response = httpclient.execute(httppost)) {
                System.out.println("----------------------------------------");
                System.out.println(response.getCode() + " " + response.getReasonPhrase());
                System.out.println(EntityUtils.toString(response.getEntity()));
            }
        }
    }
}

輸出結果:

執行請求 POST http://httpbin.org/post
----------------------------------------
200 OK
{
  "args"{}, 
  "data""{\"name\":\"www.wdbyte.com\"}\n", 
  "files"{}, 
  "form"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Content-Length""26", 
    "Content-Type""application/json; charset=UTF-8", 
    "Host""httpbin.org", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/1.8.0_151)", 
    "X-Amzn-Trace-Id""Root=1-62ee4d95-1f956d4303cea09c52694c86"
  }, 
  "json"{
    "name""www.wdbyte.com"
  }, 
  "origin""42.120.74.238", 
  "url""http://httpbin.org/post"
}

HttpClient 5 表單登錄

表單登錄可以理解爲發起一個攜帶了認證信息的請求,然後得到響應的 Cookie 的過程。當然這裏不僅僅適用於表單登錄,也可以是簡單的發起一個攜帶了表單信息的請求。

本應該使用 POST 請求發送表單參數測試,但是在 httpbin.org 中沒有對應的接口用於測試,所以這裏換成了 GET 請求

示例代碼:

package com.wdbyte.httpclient;

import java.util.ArrayList;
import java.util.List;

import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;

/**
 * 演示基於表單的登錄
 * 
 * @author https://www.wdbyte.com
 */
public class HttpClient5FormLogin {

    public static void main(final String[] args) throws Exception {
        final BasicCookieStore cookieStore = new BasicCookieStore();
        try (final CloseableHttpClient httpclient = HttpClients.custom()
                .setDefaultCookieStore(cookieStore)
                .build()) {

            // 本應該使用 POST 請求發送表單參數,但是在 httpbin.org 中沒有對應的接口用於測試,所以這裏換成了 GET 請求
            // HttpPost httpPost = new HttpPost("http://httpbin.org/cookies/set/username/wdbyte.com");
            HttpGet httpPost = new HttpGet("http://httpbin.org/cookies/set/username/wdbyte.com");
            // POST 表單請求參數
            List<NameValuePair> nvps = new ArrayList<>();
            nvps.add(new BasicNameValuePair("username""wdbyte.com"));
            nvps.add(new BasicNameValuePair("password""secret"));
            httpPost.setEntity(new UrlEncodedFormEntity(nvps));

            try (final CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
                final HttpEntity entity = response2.getEntity();

                System.out.println("Login form get: " + response2.getCode() + " " + response2.getReasonPhrase());
                System.out.println("當前響應信息 "+EntityUtils.toString(entity));;

                System.out.println("Post 登錄 Cookie:");
                final List<Cookie> cookies = cookieStore.getCookies();
                if (cookies.isEmpty()) {
                    System.out.println("None");
                } else {
                    for (int i = 0; i < cookies.size(); i++) {
                        System.out.println("- " + cookies.get(i));
                    }
                }
            }
        }
    }
}

輸出結果:

Login form get: 200 OK
當前響應信息 {
  "cookies"{
    "username""wdbyte.com"
  }
}

Post 登錄 Cookie:
- [name: username; value: wdbyte.com; domain: httpbin.org; path: /; expiry: null]

HttpClient 5 Basic Authorization

HTTP 基本認證(Basic Authorization)是一種比較簡單的認證實現,主要流程如下

  1. 1. 請求一個需要進行基本認證的 HTTP 接口,但是沒有攜帶認證信息。

  2. 2. 此時會響應 401 狀態碼,並在響應 header 中的 WWW-Authenticate 提示需要進行基本認證。

  3. 3. 用戶把需要提交認證信息進行冒號拼接,然後進行 base64 編碼,再在得到的字符串開頭拼接上 Basic 放入請求頭 Authorization 中。

  4. 4. 認證成功,響應成功。

你可以通過瀏覽器打開下面這個 URL 進行基本認證測試。

http://httpbin.org/basic-auth/admin/123456

在 Apache HttpClient 5 中的實現方式。

package com.wdbyte.httpclient;

import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.EntityUtils;

/**
 * 一個簡單的示例,它使用HttpClient執行HTTP請求;
 * 一個需要進行用戶身份驗證的目標站點。
 */
public class HttpClient5BasicAuthentication {

    public static void main(final String[] args) throws Exception {
        final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
        credsProvider.setCredentials(
                new AuthScope("httpbin.org", 80),
                new UsernamePasswordCredentials("admin""123456".toCharArray()));
        try (final CloseableHttpClient httpclient = HttpClients.custom()
                .setDefaultCredentialsProvider(credsProvider)
                .build()) {
            final HttpGet httpget = new HttpGet("http://httpbin.org/basic-auth/admin/123456");

            System.out.println("執行請求" + httpget.getMethod() + " " + httpget.getUri());
            try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
                System.out.println("----------------------------------------");
                System.out.println(response.getCode() + " " + response.getReasonPhrase());
                System.out.println(EntityUtils.toString(response.getEntity()));
            }
        }
    }
}

輸出結果:

執行請求GET http://httpbin.org/basic-auth/user/passwd
----------------------------------------
200 OK
{
  "authenticated": true, 
  "user""user"
}

通過抓包可以看到完整的 HTTP 請求響應過程。

// 請求
GET /basic-auth/user/passwd HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)
// 響應
HTTP/1.1 401 UNAUTHORIZED
Date: Sat, 06 Aug 2022 08:25:33 GMT
Content-Length: 0
Connection: keep-alive
Server: gunicorn/19.9.0
WWW-Authenticate: Basic realm="Fake Realm"
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// 請求
GET /basic-auth/user/passwd HTTP/1.1
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)
Authorization: Basic dXNlcjpwYXNzd2Q=
// 響應
HTTP/1.1 200 OK
Date: Sat, 06 Aug 2022 08:25:33 GMT
Content-Type: application/json
Content-Length: 47
Connection: keep-alive
Server: gunicorn/19.9.0
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "authenticated": true, 
  "user""user"
}

HttpClient 5 Digest Authorization

HTTP Basic Authorization 的缺點顯而易見,密碼通過明文傳輸存在一定的安全風險,Digest Authorization 認證方式解決了明文傳輸的問題,這裏不過多介紹 Digest 的相關內容,通過一個圖簡單的示意 Digest 認證方式的流程。

Digest 認證流程

下面是代碼演示。

package com.wdbyte.httpclient;

import org.apache.hc.client5.http.auth.AuthExchange;
import org.apache.hc.client5.http.auth.AuthScheme;
import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.auth.DigestScheme;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.EntityUtils;

/**
 *
 * HttpClient如何驗證多個請求的示例
 * 使用相同的摘要方案。在初始請求/響應交換之後
 * 共享相同執行上下文的所有後續請求都可以重用
 * 要向服務器進行身份驗證的最後一個摘要nonce值。
 */
public class HttpClient5PreemptiveDigestAuthentication {

    public static void main(final String[] args) throws Exception {
        try (final CloseableHttpClient httpclient = HttpClients.createDefault()) {

            final HttpHost target = new HttpHost("http""httpbin.org", 80);

            final HttpClientContext localContext = HttpClientContext.create();
            final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(
                    new AuthScope(target),
                    new UsernamePasswordCredentials("admin""123456".toCharArray()));
            localContext.setCredentialsProvider(credentialsProvider);

            final HttpGet httpget = new HttpGet("http://httpbin.org/digest-auth/auth/admin/123456");

            System.out.println("執行請求 " + httpget.getMethod() + " " + httpget.getUri());
            for (int i = 0; i < 2; i++) {
                try (final CloseableHttpResponse response = httpclient.execute(target, httpget, localContext)) {
                    System.out.println("----------------------------------------");
                    System.out.println(response.getCode() + " " + response.getReasonPhrase());
                    EntityUtils.consume(response.getEntity());

                    final AuthExchange authExchange = localContext.getAuthExchange(target);
                    if (authExchange != null) {
                        final AuthScheme authScheme = authExchange.getAuthScheme();
                        if (authScheme instanceof DigestScheme) {
                            final DigestScheme digestScheme = (DigestScheme) authScheme;
                            System.out.println("Nonce: " + digestScheme.getNonce() +
                                    "; count: " + digestScheme.getNounceCount());
                        }
                    }
                }
            }
        }
    }

}

通過抓包工具可以清晰的看到 2 次請求的流程,在最後一次請求中,直接共享了認證信息,沒有再次的重新認證的流程。

// 1. 請求
GET /digest-auth/auth/admin/123456 HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)
// 2. 詳情,提示認證,給出參數
HTTP/1.1 401 UNAUTHORIZED
Date: Fri, 12 Aug 2022 07:11:06 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: keep-alive
Server: gunicorn/19.9.0
WWW-Authenticate: Digest realm="me@kennethreitz.com"nonce="8dc5e7974a86a6fcc3cf73230b0c4a93"qop="auth"opaque="64b7f68b386c3acc38131f7472aa2079"algorithm=MD5, stale=FALSE
Set-Cookie: stale_after=never; Path=/
Set-Cookie: fake=fake_value; Path=/
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// 3. 參數+密碼 加密後再次請求
GET /digest-auth/auth/admin/123456 HTTP/1.1
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)
Cookie: fake=fake_value; stale_after=never
Authorization: Digest userme@kennethreitz.com", nonce="8dc5e7974a86a6fcc3cf73230b0c4a93", uri="/digest-auth/auth/admin/123456", response="7c6726f8ac54c1ba28e19c71b2fc7338", qop=auth, nc=00000001, cnonce="2fa61501d47a9d39", algorithm=MD5, opaque="64b7f68b386c3acc38131f7472aa2079"
// 4. 認證成功,響應
HTTP/1.1 200 OK
Date: Fri, 12 Aug 2022 07:11:08 GMT
Content-Type: application/json
Content-Length: 48
Connection: keep-alive
Server: gunicorn/19.9.0
Set-Cookie: fake=fake_value; Path=/
Set-Cookie: stale_after=never; Path=/
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "authenticated": true, 
  "user": "admin"
}
// 5. 再次請求,共享了登錄狀態。
GET /digest-auth/auth/admin/123456 HTTP/1.1
Accept-Encoding: gzip, x-gzip, deflate
Host: httpbin.org
Connection: keep-alive
User-Agent: Apache-HttpClient/5.1.3 (Java/1.8.0_151)
Cookie: fake=fake_value; stale_after=never
Authorization: Digest userme@kennethreitz.com"nonce="8dc5e7974a86a6fcc3cf73230b0c4a93"uri="/digest-auth/auth/admin/123456"response="9955ac79f6a51a876a326449447f549d"qop=auth, nc=00000002, cnonce="2fa61501d47a9d39"algorithm=MD5, opaque="64b7f68b386c3acc38131f7472aa2079"
// 5. 認證成功,響應
HTTP/1.1 200 OK
Date: Fri, 12 Aug 2022 07:11:09 GMT
Content-Type: application/json
Content-Length: 48
Connection: keep-alive
Server: gunicorn/19.9.0
Set-Cookie: fake=fake_value; Path=/
Set-Cookie: stale_after=never; Path=/
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

{
  "authenticated": true, 
  "user""admin"
}

HttpClient 5 攔截器

HttpClient 5 中的攔截器可以對請求過程的各個階段進行攔截處理,通過 HttpClientBuilder 中的關於 Interceptor 的方法可以看到可以進行攔截的節點。

HttpClient5 攔截器

下面編寫一個示例,發起三次請求,每次請求都在請求頭 herader 中增加一個 request-id 參數,然後對 request-id 值爲 2 的請求直接響應 404 結束。

package com.wdbyte.httpclient;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.classic.ExecChain.Scope;
import org.apache.hc.client5.http.classic.ExecChainHandler;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.ChainElement;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;

/**
 * 展示如何在請求和響應時進行攔截進行自定義處理。
 */
public class HttpClient5Interceptors {

    public static void main(final String[] args) throws Exception {
        try (final CloseableHttpClient httpclient = HttpClients.custom()
            // 添加一個請求 id 到請求 header
            .addRequestInterceptorFirst(new HttpRequestInterceptor() {
                private final AtomicLong count = new AtomicLong(0);
                @Override
                public void process(
                    final HttpRequest request,
                    final EntityDetails entity,
                    final HttpContext context) throws HttpException, IOException {
                    request.setHeader("request-id", Long.toString(count.incrementAndGet()));
                }
            })
            .addExecInterceptorAfter(ChainElement.PROTOCOL.name()"custom", new ExecChainHandler() {
                // 請求 id 爲 2 的,模擬 404 響應,並自定義響應的內容。
                @Override
                public ClassicHttpResponse execute(
                    final ClassicHttpRequest request,
                    final Scope scope,
                    final ExecChain chain) throws IOException, HttpException {

                    final Header idHeader = request.getFirstHeader("request-id");
                    if (idHeader != null && "2".equalsIgnoreCase(idHeader.getValue())) {
                        final ClassicHttpResponse response = new BasicClassicHttpResponse(HttpStatus.SC_NOT_FOUND,
                            "Oppsie");
                        response.setEntity(new StringEntity("bad luck", ContentType.TEXT_PLAIN));
                        return response;
                    } else {
                        return chain.proceed(request, scope);
                    }
                }
            })
            .build()) {

            for (int i = 0; i < 3; i++) {
                final HttpGet httpget = new HttpGet("http://httpbin.org/get");

                try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
                    System.out.println("----------------------------------------");
                    System.out.println("執行請求 " + httpget.getMethod() + " " + httpget.getUri());
                    System.out.println(response.getCode() + " " + response.getReasonPhrase());
                    System.out.println(EntityUtils.toString(response.getEntity()));
                }
            }
        }
    }

}

輸出結果。

----------------------------------------
執行請求 GET http://httpbin.org/get
200 OK
{
  "args"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "Request-Id""1", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/1.8.0_151)", 
    "X-Amzn-Trace-Id""Root=1-62f615ba-658ccd42182d22534dbba82c"
  }, 
  "origin""42.120.75.221", 
  "url""http://httpbin.org/get"
}

----------------------------------------
執行請求 GET http://httpbin.org/get
404 Oppsie
bad luck
----------------------------------------
執行請求 GET http://httpbin.org/get
200 OK
{
  "args"{}, 
  "headers"{
    "Accept-Encoding""gzip, x-gzip, deflate", 
    "Host""httpbin.org", 
    "Request-Id""3", 
    "User-Agent""Apache-HttpClient/5.1.3 (Java/1.8.0_151)", 
    "X-Amzn-Trace-Id""Root=1-62f615bb-4eb6ba10736ace0e21d0cb8c"
  }, 
  "origin""42.120.75.221", 
  "url""http://httpbin.org/get"
}

一如既往,文章代碼都存放在 Github.com/niumoo/javaNotes[1].

引用鏈接

[1] Github.com/niumoo/javaNotes: https://github.com/niumoo/JavaNotes/tree/master/tool-java-apache-httpclient

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qGG9UcI_mHCnhEYlg6ToNA