Javaでの新しいHTTPクライアントの調査
1. 概要
このチュートリアルでは、Java11の標準化について説明します。
これは、Javaのごく初期の頃からJDKに存在していたレガシーHttpUrlConnectionクラスを置き換えることを目的としています。
ごく最近まで、Javaは HttpURLConnection APIのみを提供していました。これは低レベルであり、機能が豊富なおよびユーザーフレンドリーであることが知られていません。 。
そのため、 Apache HttpClient 、 Jetty 、Springの RestTemplate など、広く使用されているサードパーティライブラリが一般的に使用されていました。
2. バックグラウンド
この変更は、JEP321の一部として実装されました。
2.1. JEP321の一部としての主な変更
- Java9のインキュベートされたHTTPAPIは、JavaSEAPIに正式に組み込まれました。 新しいHTTPAPI は、java.net.HTTP。*にあります。
- 新しいバージョンのHTTPプロトコルは、クライアントによる要求の送信とサーバーからの応答の受信の全体的なパフォーマンスを向上させるように設計されています。 これは、ストリームの多重化、ヘッダーの圧縮、プッシュプロミスなどの多くの変更を導入することで実現されます。
- Java 11以降、 APIは完全に非同期になりました(以前のHTTP / 1.1実装はブロックされていました)。非同期呼び出しはCompleteableFutureを使用して実装されます。CompletableFuture実装前のステージが終了すると、各ステージの適用を処理するため、このフロー全体は非同期です。
- 新しいHTTPクライアントAPIは、サードパーティの依存関係を追加することなく、HTTP/2などの最新のWeb機能をサポートするHTTPネットワーク操作を実行するための標準的な方法を提供します。
- 新しいAPIは、HTTP 1.1 /2WebSocketのネイティブサポートを提供します。 コア機能を提供するコアクラスとインターフェイスには、次のものがあります。
- HttpClient クラス、 java .net.http.HttpClient
- HttpRequest クラス、 java.net.http.HttpRequest
- The HttpResponse
インターフェース、 java.net.http.HttpResponse - WebSocket インターフェース、 java.net.http.WebSocket
2.2. Java11より前のHTTPクライアントに関する問題
既存のHttpURLConnection APIとその実装には、多くの問題がありました。
- URLConnection APIは、現在機能していない複数のプロトコル(FTP、gopherなど)を使用して設計されました。
- APIはHTTP/1.1よりも前のものであり、抽象的すぎます。
- ブロッキングモードでのみ機能します(つまり、要求/応答ごとに1つのスレッド)。
- 維持するのは非常に難しいです。
3. HTTPクライアントAPIの概要
HttpURLConnection とは異なり、HTTPクライアントは同期および非同期の要求メカニズムを提供します。
APIは、次の3つのコアクラスで構成されています。
- HttpRequest は、HttpClientを介して送信されるリクエストを表します。
- HttpClient は、複数のリクエストに共通する構成情報のコンテナーとして動作します。
- HttpResponse は、HttpRequest呼び出しの結果を表します。
次のセクションで、それぞれについて詳しく説明します。 まず、リクエストに焦点を当てましょう。
4. HttpRequest
HttpRequest は、送信するリクエストを表すオブジェクトです。 HttpRequest.Builder。を使用して新しいインスタンスを作成できます
HttpRequest.newBuilder()を呼び出すことで取得できます。 Builder クラスは、リクエストの構成に使用できる一連のメソッドを提供します。
最も重要なものをカバーします。
注:JDK 16には、新しいものがあります HttpRequest.newBuilder(HttpRequestリクエスト、BiPredicate
このビルダーを使用すると、元のビルダーと同等の HttpRequest をビルドできますが、ヘッダーを削除するなど、構築前にリクエストの状態を修正できます。
HttpRequest.newBuilder(request, (name, value) -> !name.equalsIgnoreCase("Foo-Bar"))
4.1. URIの設定
リクエストを作成するときに最初に行う必要があるのは、URLを提供することです。
これは、BuilderのコンストラクターをURIパラメーターとともに使用する方法と、 Builderのメソッドuri(URI)を呼び出す方法の2つで実行できます。実例:
HttpRequest.newBuilder(new URI("https://postman-echo.com/get"))
HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
基本的なリクエストを作成するために最後に構成する必要があるのは、HTTPメソッドです。
4.2. HTTPメソッドの指定
Builder からメソッドの1つを呼び出すことにより、リクエストが使用するHTTPメソッドを定義できます。
- 得る()
- POST(BodyPublisher本文)
- PUT(BodyPublisher body)
- 消去()
BodyPublisherについては後で詳しく説明します。
次に、非常に単純なGETリクエストの例を作成しましょう。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.GET()
.build();
このリクエストには、HttpClientに必要なすべてのパラメータが含まれています。
ただし、リクエストにパラメータを追加する必要がある場合があります。 ここにいくつかの重要なものがあります:
- HTTPプロトコルのバージョン
- ヘッダー
- タイムアウト
4.3. HTTPプロトコルバージョンの設定
APIはHTTP/2プロトコルを完全に活用し、デフォルトでそれを使用しますが、使用するプロトコルのバージョンを定義できます。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
ここで重要なのは、HTTP / 2がサポートされていない場合、クライアントはたとえばHTTP/1.1にフォールバックするということです。
4.4. ヘッダーの設定
リクエストにヘッダーを追加したい場合は、提供されているビルダーメソッドを使用できます。
これを行うには、すべてのヘッダーをキーと値のペアとして headers()メソッドに渡すか、単一のキーと値のヘッダーに header()メソッドを使用します。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.headers("key1", "value1", "key2", "value2")
.GET()
.build();
HttpRequest request2 = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.header("key1", "value1")
.header("key2", "value2")
.GET()
.build();
リクエストをカスタマイズするために使用できる最後の便利なメソッドは、 timeout()です。
4.5. タイムアウトの設定
次に、応答を待機する時間を定義します。
設定した時間が経過すると、HttpTimeoutExceptionがスローされます。 デフォルトのタイムアウトは無限大に設定されています。
タイムアウトは、ビルダーインスタンスでメソッド timeout()を呼び出すことにより、durationオブジェクトで設定できます。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.timeout(Duration.of(10, SECONDS))
.GET()
.build();
5. リクエスト本文の設定
リクエストビルダーメソッドPOST(BodyPublisher body)、 PUT(BodyPublisher body)、 DELETE()を使用して、リクエストに本文を追加できます。
新しいAPIは、リクエスト本文の受け渡しを簡素化する、すぐに使用できる多数のBodyPublisher実装を提供します。
- StringProcessor – HttpRequest.BodyPublishers.ofStringで作成されたStringから本文を読み取ります
- InputStreamProcessor – HttpRequest.BodyPublishers.ofInputStreamで作成されたInputStreamから本文を読み取ります
- ByteArrayProcessor – HttpRequest.BodyPublishers.ofByteArrayで作成されたバイト配列から本文を読み取ります
- FileProcessor – HttpRequest.BodyPublishers.ofFile で作成された、指定されたパスのファイルから本文を読み取ります
本文が必要ない場合は、HttpRequest.BodyPublishers。 noBody()を渡すだけです。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
注:JDK 16には、一連のパブリッシャーによって公開されたリクエストボディの連結からリクエストボディを構築するのに役立つ新しい HttpRequest.BodyPublishers.concat(BodyPublisher…)メソッドがあります。 連結パブリッシャーによって公開されたリクエスト本文は、各パブリッシャーのすべてのバイトを順番に連結することによって公開されたであろうリクエスト本文と論理的に同等です。
5.1. StringBodyPublisher
BodyPublishers の実装を使用したリクエスト本文の設定は、非常にシンプルで直感的です。
たとえば、単純な String を本文として渡したい場合は、StringBodyPublishersを使用できます。
すでに述べたように、このオブジェクトはファクトリメソッド ofString()を使用して作成できます。引数として String オブジェクトのみを受け取り、そこから本体を作成します。
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();
5.2. InputStreamBodyPublisher
そのためには、 InputStream をサプライヤーとして渡す必要があるため(作成を怠惰にするため)、StringBodyPublishersとは少し異なります。
ただし、これも非常に簡単です。
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();
ここで、単純なByteArrayInputStreamをどのように使用したかに注目してください。 もちろん、それはどのInputStream実装でもかまいません。
5.3. ByteArrayProcessor
ByteArrayProcessor を使用して、パラメーターとしてバイトの配列を渡すこともできます。
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();
5.4. FileProcessor
ファイルを操作するには、提供されているFileProcessorを利用できます。
そのファクトリメソッドは、ファイルへのパスをパラメータとして受け取り、コンテンツから本文を作成します。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.fromFile(
Paths.get("src/test/resources/sample.txt")))
.build();
HttpRequest を作成する方法と、それに追加のパラメーターを設定する方法について説明しました。
次に、要求の送信と応答の受信を担当するHttpClientクラスについて詳しく見ていきます。
6. HttpClient
すべてのリクエストはHttpClientを使用して送信されます。これは、 HttpClient.newBuilder()メソッドを使用するか、 HttpClient.newHttpClient()を呼び出すことでインスタンス化できます。
これは、要求/応答を処理するために使用できる多くの便利で自己記述的なメソッドを提供します。
ここでこれらのいくつかをカバーしましょう。
6.1. レスポンスボディの取り扱い
パブリッシャーを作成するための流暢なメソッドと同様に、一般的なボディタイプのハンドラーを作成するための専用のメソッドがあります。
BodyHandlers.ofByteArray
BodyHandlers.ofString
BodyHandlers.ofFile
BodyHandlers.discarding
BodyHandlers.replacing
BodyHandlers.ofLines
BodyHandlers.fromLineSubscriber
新しいBodyHandlersファクトリークラスの使用法に注意してください。
Java 11より前は、次のようなことをしなければなりませんでした。
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandler.asString());
そして今、それを単純化することができます:
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
6.2. プロキシの設定
Builderインスタンスでproxy()メソッドを呼び出すだけで、接続のプロキシを定義できます。
HttpResponse<String> response = HttpClient
.newBuilder()
.proxy(ProxySelector.getDefault())
.build()
.send(request, BodyHandlers.ofString());
この例では、デフォルトのシステムプロキシを使用しました。
6.3. リダイレクトポリシーの設定
アクセスしたいページが別のアドレスに移動することがあります。
その場合、通常は新しいURIに関する情報を含むHTTPステータスコード3xxを受け取ります。
BuilderのfollowRedirects()メソッドを使用してこれを行うことができます。
HttpResponse<String> response = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build()
.send(request, BodyHandlers.ofString());
すべてのポリシーは、列挙型HttpClient.Redirectで定義および記述されています。
6.4. 接続のオーセンティケーターの設定
Authenticator は、接続の資格情報(HTTP認証)をネゴシエートするオブジェクトです。
さまざまな認証スキーム(基本認証やダイジェスト認証など)を提供します。
ほとんどの場合、認証にはサーバーに接続するためのユーザー名とパスワードが必要です。
PasswordAuthentication クラスを使用できます。これは、次の値の単なるホルダーです。
HttpResponse<String> response = HttpClient.newBuilder()
.authenticator(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(
"username",
"password".toCharArray());
}
}).build()
.send(request, BodyHandlers.ofString());
ここでは、ユーザー名とパスワードの値をプレーンテキストとして渡しました。 もちろん、これは本番シナリオでは異なる必要があります。
すべてのリクエストで同じユーザー名とパスワードを使用する必要があるわけではないことに注意してください。 Authenticator クラスは、提供する値を見つけるために使用できるいくつかの getXXX (たとえば、 getRequestingSite())メソッドを提供します。
次に、新しい HttpClient の最も便利な機能の1つである、サーバーへの非同期呼び出しについて説明します。
6.5. リクエストの送信–同期と非同期
新しいHttpClientは、サーバーにリクエストを送信するための2つの可能性を提供します。
- send(…)–同期的に(応答が来るまでブロックします)
- sendAsync(…)–非同期(応答を待たず、非ブロッキング)
これまで、 send(。 ..)メソッドは自然に応答を待ちます。
HttpResponse<String> response = HttpClient.newBuilder()
.build()
.send(request, BodyHandlers.ofString());
この呼び出しはHttpResponseオブジェクトを返し、アプリケーションフローからの次の命令は、応答がすでにここにある場合にのみ実行されると確信しています。
ただし、特に大量のデータを処理する場合には、多くの欠点があります。
だから、今私たちは使用することができます sendAsync(。 ..)メソッド—これは CompletableFeature
CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
.build()
.sendAsync(request, HttpResponse.BodyHandlers.ofString());
新しいAPIは、複数の応答を処理し、要求と応答の本文をストリーミングすることもできます。
List<URI> targets = Arrays.asList(
new URI("https://postman-echo.com/get?foo1=bar1"),
new URI("https://postman-echo.com/get?foo2=bar2"));
HttpClient client = HttpClient.newHttpClient();
List<CompletableFuture<String>> futures = targets.stream()
.map(target -> client
.sendAsync(
HttpRequest.newBuilder(target).GET().build(),
HttpResponse.BodyHandlers.ofString())
.thenApply(response -> response.body()))
.collect(Collectors.toList());
6.6. 非同期呼び出しのExecutorの設定
非同期呼び出しで使用されるスレッドを提供するExecutorを定義することもできます。
このようにして、たとえば、リクエストの処理に使用されるスレッドの数を制限できます。
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());
デフォルトでは、 HttpClientはエグゼキューターjava.util.concurrent.Executors.newCachedThreadPool()を使用します。
6.7. CookieHandlerの定義
新しいAPIとビルダーを使用すると、接続にCookieHandlerを簡単に設定できます。 ビルダーメソッドcookieHandler(CookieHandler cookieHandler)を使用して、クライアント固有のCookieHandlerを定義できます。
CookieManager(Cookieの保存をCookieの受け入れと拒否に関するポリシーから分離するCookieHandler の具体的な実装)を定義して、Cookieの受け入れをまったく許可しないようにします。
HttpClient.newBuilder()
.cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
.build();
CookieManager でCookieの保存が許可されている場合は、HttpClientからCookieHandlerを確認してCookieにアクセスできます。
((CookieManager) httpClient.cookieHandler().get()).getCookieStore()
次に、HttpAPIの最後のクラスであるHttpResponseに注目しましょう。
7. HttpResponseオブジェクト
HttpResponse クラスは、サーバーからの応答を表します。 これは多くの便利な方法を提供しますが、これらは2つの最も重要な方法です。
- statusCode()は、応答のステータスコード(タイプ int )を返します( HttpURLConnection クラスには可能な値が含まれます)。
- body()は、応答の本文を返します(戻りタイプは、 send()メソッドに渡される応答 BodyHandler パラメーターによって異なります)。
応答オブジェクトには、 uri()、 headers()、 trailers()、 version()などの他の便利なメソッドがあります。 。
7.1. 応答オブジェクトのURI
応答オブジェクトのメソッドuri()は、応答を受信したURIを返します。
リダイレクトが発生する可能性があるため、リクエストオブジェクトのURIとは異なる場合があります。
assertThat(request.uri()
.toString(), equalTo("http://stackoverflow.com"));
assertThat(response.uri()
.toString(), equalTo("https://stackoverflow.com/"));
7.2. 応答からのヘッダー
応答オブジェクトでメソッドheaders()を呼び出すことにより、応答からヘッダーを取得できます。
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
HttpHeaders responseHeaders = response.headers();
HttpHeaders オブジェクトを返します。これは、HTTPヘッダーの読み取り専用ビューを表します。
ヘッダー値の検索を簡素化するいくつかの便利なメソッドがあります。
7.3. 応答のバージョン
メソッドversion()は、サーバーとの通信に使用されたHTTPプロトコルのバージョンを定義します。
HTTP / 2を使用することを定義した場合でも、サーバーはHTTP/1.1を介して応答できることに注意してください。
サーバーが応答したバージョンは、応答で指定されます。
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/get"))
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
assertThat(response.version(), equalTo(HttpClient.Version.HTTP_1_1));
8. HTTP/2でのプッシュプロミスの処理
新しい HttpClient プッシュプロミスをサポート PushPromiseHandler インターフェース
これにより、サーバーはプライマリリソースを要求しながらコンテンツをクライアントに「プッシュ」し、ラウンドトリップを節約し、その結果、ページレンダリングのパフォーマンスを向上させることができます。
リソースのバンドルを忘れることができるのは、実際にはHTTP/2の多重化機能です。 サーバーは、リソースごとに、プッシュプロミスと呼ばれる特別な要求をクライアントに送信します。
受信したプッシュプロミスは、指定されたPushPromiseHandlerによって処理されます。 null値のPushPromiseHandlerは、プッシュプロミスを拒否します。
HttpClient には、オーバーロードされた sendAsync メソッドがあり、以下に示すように、そのような約束を処理できます。
まず、PushPromiseHandlerを作成しましょう。
private static PushPromiseHandler<String> pushPromiseHandler() {
return (HttpRequest initiatingRequest,
HttpRequest pushPromiseRequest,
Function<HttpResponse.BodyHandler<String>,
CompletableFuture<HttpResponse<String>>> acceptor) -> {
acceptor.apply(BodyHandlers.ofString())
.thenAccept(resp -> {
System.out.println(" Pushed response: " + resp.uri() + ", headers: " + resp.headers());
});
System.out.println("Promise request: " + pushPromiseRequest.uri());
System.out.println("Promise request: " + pushPromiseRequest.headers());
};
}
次に、 sendAsync メソッドを使用して、このプッシュプロミスを処理しましょう。
httpClient.sendAsync(pageRequest, BodyHandlers.ofString(), pushPromiseHandler())
.thenAccept(pageResponse -> {
System.out.println("Page response status code: " + pageResponse.statusCode());
System.out.println("Page response headers: " + pageResponse.headers());
String responseBody = pageResponse.body();
System.out.println(responseBody);
})
.join();
9. 結論
この記事では、Java9で導入されたインキュベーション用HttpClientをより強力な変更で標準化したJava11 HttpClientAPIについて説明しました。
使用される完全なコードは、GitHubのにあります。
例では、https://postman-echo.comによって提供されるサンプルRESTエンドポイントを使用しました。