【Modern Java】java.net.http.HttpClient (JEP 321)

f:id:Naotsugu:20200724174249p:plain


blog1.mammb.com


JEP 321: HTTP Client (Standard)

Java 9 で Incubator として入った HTTP Client (JEP 110: HTTP/2 Client (Incubator)) が Java 11 で標準導入となりました。

古い標準APIで HTTP 接続を行うには HttpURLConnection を使い、以下のように実装することができます。

static void get(URL url) {
    HttpURLConnection conn = null;
    try {
        conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        int code = conn.getResponseCode();
        if (code != HttpURLConnection.HTTP_OK) {
            System.out.println("ResponseCode:" + code);
            return;
        }
        try (InputStream is = conn.getInputStream()) {
            System.out.println(
                new String(is.readAllBytes(), StandardCharsets.UTF_8));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (Objects.nonNull(conn)) conn.disconnect();
    }
}

URLConnection は ftp や gopher など複数のプロトコルを扱うよう設計されていますが、そのほとんどのプロトコルは現在では利用されていません。 API は HTTP/1.1 以前のもので抽象的すぎて使いづらく、メンテナンスも難しいもとなっています。 さらに、リクエスト/レスポンスごとに 1 つのスレッド使ったブロッキングモードでしか動かすことができません。

そこで、従来の HttpURLConnection API を置き換える新しい HTTP クライアント API が定義されました(HTTP/2 と WebSocket のサポートあり)。


中心となるクラスは以下です。

  • java.net.http.HttpClient
  • java.net.http.HttpRequest
  • java.net.http.HttpRequest.BodyPublisher
  • java.net.http.HttpResponse
  • java.net.http.HttpResponse.BodyHandler


Http Client で GET リクエスト

HttpClient を作成し HttpRequest を渡すと HttpResponse が帰ってきます。

以下のようになります。

HttpClient client = HttpClient.newBuilder().build();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://blog1.mammb.com/"))
        .timeout(Duration.ofSeconds(15))
        .build();

HttpResponse<String> res = client.send(req,
        HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

System.out.println(res.body());

HttpClient は HttpClient client = HttpClient.newHttpClient(); のようにインスタンス化することもできます。

BodyHandlerHttpResponse.BodyHandlers に以下のようなファクトリが用意されています。

  • BodyHandlers::ofByteArray
  • BodyHandlers::ofFile
  • BodyHandlers::ofString
  • BodyHandlers::ofInputStream


Http Client で POST リクエスト

BodyPublisher を使って POST するボディを指定します。

HttpClient client = HttpClient.newBuilder().build();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://blog1.mammb.com/"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofFile(Paths.get("file.json")))
        .build();

HttpResponse<String> res = client.send(req,
        HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

System.out.println(res.body());

BodyPublishers には以下のようなファクトリが用意されています。

  • BodyPublishers::ofString
  • BodyPublishers::ofFile
  • BodyPublishers::ofByteArray
  • BodyPublishers::ofInputStream
  • BodyPublishers::noBody


非同期リクエスト

HttpClient.sendAsync() で非同期リクエストにすることができます。

client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept(System.out::println)
        .get();

戻り値は CompletableFuture となるため、複数のリクエストを並列に実行して、すべての完了を待ち合わせるといったことが簡単に実現できます。

HttpClient client = HttpClient.newHttpClient();

List<HttpRequest> requests = uris.stream()
        .map(HttpRequest::newBuilder)
        .map(HttpRequest.Builder::build)
        .collect(Collectors.toList());

CompletableFuture.allOf(requests.stream()
        .map(request -> client.sendAsync(request, ofString()))
        .toArray(CompletableFuture<?>[]::new))
        .join();


Json レスポンスからオブジェクトへマッピング

HttpClient には Json に対するサポートがありませんが、BodyHandler を自作すれば簡単に対応が可能です。

JSON-B の RI である yasson を使う場合は以下のような依存を追加します。

dependencies {
    implementation 'org.eclipse:yasson:1.0.7'
}

BodyHandler を生成するユーティリティを以下のように作成します。

import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import java.io.ByteArrayInputStream;
import java.net.http.HttpResponse;
import java.util.Objects;

public final class ExtBodyHandlers {

    private static final Jsonb jsonb = JsonbBuilder.create();

    private ExtBodyHandlers() {
    }

    public static <T> HttpResponse.BodyHandler<T> ofObjectAsJson(final Class<T> type) {
        Objects.requireNonNull(type);
        return (responseInfo) -> HttpResponse.BodySubscribers.mapping(
                HttpResponse.BodySubscribers.ofByteArray(),
                byteArray -> jsonb.fromJson(new ByteArrayInputStream(byteArray), type));
    }
}

Jsonb はスレッドセーフなので、スタティックに定義して問題ありません。

例として NTP の結果が Json 形式で取得できるので、以下のようにすることで、レスポンスとして直接 Json からオブジェクトにマッピングした結果として取得できます。

HttpClient client = HttpClient.newBuilder().build();

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://ntp-a1.nict.go.jp/cgi-bin/json"))
        .build();

HttpResponse<Ntp> res = client.send(req, ExtBodyHandlers.ofObjectAsJson(Ntp.class));
System.out.println(res.body().toString());

ここで Ntp は以下のような Bean です。

public class Ntp {
    private String id;
    private float it;
    private float st;
    private long leap;
    private long next;
    private long step;
    public Ntp() {   }
    // getter / setter / toString
}

以下のような結果が得られます。

Ntp[id='ntp-a1.nict.go.jp', it=0.0, st=1.59602214E9, leap=36, next=1483228800, step=1]


Json の POST

前述の Jsonb を使えば、Json の POST は単に以下のようにするだけです。

HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://blog1.mammb.com/"))
        .POST(BodyPublishers.ofString(jsonb.toJson(Foo)))
        .header("Content-Type", "application/json")
        .build();


HttpRequest.Builder のコピー

HttpRequest.Builder をコピーして同じような複数のリクエストを生成できます。

HttpRequest.Builder builder = HttpRequest.newBuilder()
        .uri(URI.create("https://blog1.mammb.com/"));

HttpRequest req1 = builder.copy().setHeader("X-Counter", "1").build();
HttpRequest req2 = builder.copy().setHeader("X-Counter", "2").build();


レスポンスヘッダ

HttpResponse.headers() よりヘッダを取得できます。

HttpResponse<String> res = client.send(request, BodyHandlers.ofString());
res.headers().allValues("X-Custom-Header").forEach(System.out::println)

firstValue()firstValueAsLong() などで取得することもできます。


CookieManager はデフォルトで無効化されているため、以下のように明示的に有効化する必要があります。

HttpClient client = HttpClient.newBuilder()
        .cookieHandler(new CookieManager())
        .build();

または以下のようにすることもできます。

CookieHandler.setDefault(new CookieManager());

HttpClient  client = HttpClient.newBuilder()
        .cookieHandler(CookieHandler.getDefault())
        .build();

CookieManager を有効化すれば HttpClient によって透過的に処理されるため、リクエストやレスポンスに特別な処理は不要です。


リダイレクト

followRedirects で指定します。

HttpClient client = HttpClient.newBuilder()
        .followRedirects(HttpClient.Redirect.ALWAYS)
        .build();

Redirect は以下の定義となっています。

public enum Redirect {
    /** Never redirect. */
    NEVER,

    /** Always redirect. */
    ALWAYS,

    /** Always redirect, except from HTTPS URLs to HTTP URLs. */
    NORMAL
}

デフォルトは Redirect.NEVER です。


Proxy

ProxySelector を使います。

HttpClient client = HttpClient.newBuilder()
        .proxy(ProxySelector.of(new InetSocketAddress(PROXY_HOST, PROXY_PORT)))
        .build();

システムワイドの Proxy 設定を使う場合は以下のようにすることもできます。

HttpClient client = HttpClient.newBuilder()
        .proxy(ProxySelector.getDefault())
        .build();


ファイルのダウンロード

BodyHandlers.ofFile によりレスポンスをファイルとして保存することができます。 これによりファイルダウンロードを行うことができます。

Path localFile = Paths.get("7z.exe");
HttpResponse<Path> res = client.send(request,
        BodyHandlers.ofFile(localFile));


まとめ

Java 11 で正式版となった HttpClient について見てきました。

UrlBuilder などが無いため、クエリパラメータなど含めた URI の構築が多少面倒な点があります。 Java URL builder などのライブラリを合わせて使うとよいでしょう。

また、HttpClient 自体には Json のサポートは無いため、JSON-BGson などを使うのが現実的でしょう。

いずれにせよ、HttpURLConnection はもう使うことはなくなるでしょう。