2023/06/05

HttpClient in JDK 11

JDK 8 以前的 HTTP Client 通常是使用第三方函式庫,最常使用的是 Apache HTTPComponents 及 OkHttp。在 JDK 9 以後,標準函式庫裡面也有 HTTP Client 可以使用。

程式包含三個部分:HttpRequest, HttpResponse 及 HttpClient

HttpRequst

  1. URI 就是 request 要發送的目標網址

  2. HTTP Method

    • GET()
    • POST(BodyPublisher body)
    • PUT(BodyPublisher body)
    • DELETE()
  3. HTTP Protcol Version

    可指定這個 http request 的版本,以往常見的是 1.1,目前是 2

  4. Headers

    設定 http request header

  5. Timeout

    設定等待 http response 的 timeout 時間

  6. Request Body

    如果是 POST, PUT, DELETE method,需要再增加 body content,對應的 content

Request Body

可使用以下這些 API 實作 body content

  1. StringProcessor

    從 String 產生 body,以 HttpRequest.BodyPublishers.ofString 產生

    HttpRequest request = HttpRequest.newBuilder()
     .uri(new URI("https://postman-echo.com/post"))
     .headers("Content-Type", "text/plain;charset=UTF-8")
     .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
     .build();
  2. IputStreamProcessor

    從 InputSteam 產生 body,以 HttpRequest.BodyPublishers.ofInputStream 產生

    byte[] sampleData = "Sample request body".getBytes();
    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers
                   .ofInputStream(() -> new ByteArrayInputStream(sampleData)))
           .build();
  3. ByteArrayProcessor

    從 ByteArray 產生 Body,以 HttpRequest.BodyPublishers.ofByteArray 產生

    byte[] sampleData = "Sample request body".getBytes();
    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
           .build();
  4. FileProcessor

    從某個路徑的檔案產生 body,以 HttpRequest.BodyPublishers.ofFile 產生

    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers.ofFile(
                   Paths.get("sample.txt")))
           .build();
  5. noBody

    如果沒有 body content,可使用 HttpRequest.BodyPublishers.**noBody()

    HttpRequest request = HttpRequest.newBuilder()
     .uri(new URI("https://postman-echo.com/post"))
     .POST(HttpRequest.BodyPublishers.noBody())
     .build();

HttpClient

  • 透過 HttpClient.newBuilder() 或是 HttpClient.newHttpClient() 產生 instance

  • 透過 Handler 處理 response body

    BodyHandlers.ofByteArray
    BodyHandlers.ofString
    BodyHandlers.ofFile
    BodyHandlers.discarding
    BodyHandlers.replacing
    BodyHandlers.ofLines
    BodyHandlers.fromLineSubscriber
    
    // jdk 11 以前
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandler.asString());
    
    // 新的 jdk
    HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
  • Proxy

    可定義 proxy

    HttpResponse<String> response = HttpClient
      .newBuilder()
      .proxy(ProxySelector.getDefault())
      .build()
      .send(request, BodyHandlers.ofString());
  • Direct Policy

    如果 reponse 收到 3XX 的 redirect 結果,可設定 redirect policy 直接轉址

    HttpResponse<String> response = HttpClient.newBuilder()
      .followRedirects(HttpClient.Redirect.ALWAYS)
      .build()
      .send(request, BodyHandlers.ofString());
  • HTTP Authentication

    HttpResponse<String> response = HttpClient.newBuilder()
      .authenticator(new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
          return new PasswordAuthentication(
            "username", 
            "password".toCharArray());
        }
    }).build()
      .send(request, BodyHandlers.ofString());
  • Cookie

    // 透過 cookieHandler(CookieHandler cookieHandler)  定義 CookieHandler
    // 設定不接受 cookie
    HttpClient.newBuilder()
      .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
      .build();
    
    // 取得 CookieStore
    ((CookieManager) httpClient.cookieHandler().get()).getCookieStore()
  • SSL Context

    在 HttpClient 指定 SSL Context,忽略 ssl key 檢查

    private static SSLContext disabledSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            // https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms
            sslContext.init(
                    null,
                new TrustManager[] {
                    new X509TrustManager() {
                        public X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }
    
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                        }
    
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                        }
                    }
                },
                new SecureRandom()
            );
            return sslContext;
        }

Sync or Async 同步或是非同步呼叫

HttpClient 有同步或非同步的發送 request 的方式

  • send 同步

    程式會停在這邊,等待 response 或是 timeout,下一行,可直接取得 response body

    HttpResponse<String> response = HttpClient.newBuilder()
      .build()
      .send(request, BodyHandlers.ofString());
  • sendAsync 非同步 non-blocking

    CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());
  • 可指定 Executor 限制 threads

    預設是使用 java.util.concurrent.Executors.newCachedThreadPool()

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    
    CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
      .executor(executorService)
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());
    
    CompletableFuture<HttpResponse<String>> response2 = HttpClient.newBuilder()
      .executor(executorService)
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());

HttpResponse

  • URI

    HttpResponse 也有一個 uri() method,可取得 uri,因為有時候會遇到 redirect uri 的回應,因此 response 的 uri,會取得 redirect 後的網址

  • Response Header

    取得 response header list

    HttpResponse<String> response = HttpClient.newHttpClient()
      .send(request, HttpResponse.BodyHandlers.ofString());
    HttpHeaders responseHeaders = response.headers();
  • Http Version

    server 是以哪一個 http version 回應的

    response.version();
  • Response Body

    String body = response.body();

完整 Java Code

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class HttpClientTest {

    public static void main(String[] args) throws Exception {

        // 建立HttpClient實例
        HttpClient httpClient = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1) // http 1.1
                .connectTimeout(Duration.ofSeconds(5)) // timeout after 5 seconds
                .sslContext(disabledSSLContext()) // disable SSL verify
                .build();

        // 建立 HttpRequest請求
//        HttpRequest request = getMethod();
//        HttpRequest request = postNoBody();
        HttpRequest request = postStringBody();
//        HttpRequest request = postInputStreamBody();
//        HttpRequest request = postByteArrayBody();
//        HttpRequest request = postFileBody();

        // 錯誤的 URI 測試 Timeout
//        HttpRequest request = postStringBody_InvalidUri();

        // Sync 同步呼叫
//        sync(httpClient, request);
        // 非同步呼叫
        async(httpClient, request);

    }

    private static void async(HttpClient httpClient, HttpRequest request) throws InterruptedException, java.util.concurrent.ExecutionException, java.util.concurrent.TimeoutException {
        // 非同步
        CompletableFuture<HttpResponse<String>> response = httpClient
                .sendAsync(request, HttpResponse.BodyHandlers.ofString());
        String result = response
                .thenApply(HttpResponse::body)
                .exceptionally(t -> {
                    t.printStackTrace();
                    return "fallback";
                })
                .get(10, TimeUnit.SECONDS);
        System.out.println(result);
    }

    private static void sync(HttpClient httpClient, HttpRequest request) throws java.io.IOException, InterruptedException {
        // 發送請求並接收回應
        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        HttpClient.Version version = response.version();
        System.out.println("---response version---");
        System.out.println(version);

        System.out.println("---response headers---");
        HttpHeaders responseHeaders = response.headers();
        Map<String, List<String>> responseHeadersMap = responseHeaders.map();
        for (String key : responseHeadersMap.keySet()) {
            System.out.println(key + ":" + responseHeadersMap.get(key));
        }

        // 取得回應主體內容
        String body = response.body();
        System.out.println("---response body---");
        System.out.println(body);
    }

    private static HttpRequest getMethod() {
        // 臺灣證券交易所0056個股日成交資訊API
        String url = "https://www.twse.com.tw/exchangeReport/STOCK_DAY?response=json&date=20230531&stockNo=0056";

        // 建立 HttpRequest請求  get method
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .version(HttpClient.Version.HTTP_2)
                .header("cache-control", "no-cache")
                .GET()
                .build();
        return request;
    }

    private static HttpRequest postNoBody() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .POST(HttpRequest.BodyPublishers.noBody())
                .build();
        return request;
    }

    private static HttpRequest postStringBody() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
                .build();
        return request;
    }

    private static HttpRequest postInputStreamBody() throws URISyntaxException {
        byte[] sampleData = "Sample request body".getBytes();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers
                        .ofInputStream(() -> new ByteArrayInputStream(sampleData)))
                .build();
        return request;
    }

    private static HttpRequest postByteArrayBody() throws URISyntaxException {
        byte[] sampleData = "Sample request body".getBytes();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
                .build();
        return request;
    }

    private static HttpRequest postFileBody() throws URISyntaxException, FileNotFoundException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofFile(
                        Paths.get("sample.txt")))
                .build();
        return request;
    }

    private static HttpRequest postStringBody_InvalidUri() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://test.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
                .build();
        return request;
    }

    private static SSLContext disabledSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        // https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms
        sslContext.init(
                null,
            new TrustManager[] {
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    public void checkClientTrusted(X509Certificate[] certs, String authType) {
                    }

                    public void checkServerTrusted(X509Certificate[] certs, String authType) {
                    }
                }
            },
            new SecureRandom()
        );
        return sslContext;
    }

}

References

菜鳥工程師 肉豬: Java 11 HttpClient發送請求範例

Exploring the New HTTP Client in Java | Baeldung

沒有留言:

張貼留言