🍍

6.2 HTTPによるアプリケーション連携(RESTサービス、HTTPクライアントAPI、非同期呼び出しなど)~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

6.2 HTTPによるアプリケーション連携

チャプターの概要

このチャプターでは、HTTPの基本的な仕様や、HTTPクライアントの構築方法について学びます。

6.2.1 HTTPの特徴とWeb API

このレッスンではHTTPによるアプリケーション連携に入る前に、URLやメソッド、ステータスといったHTTPに関する様々な仕様について説明します。また、Web APIやRESTサービスの概念についても取り上げます。
これらの知識は、HTTPによるアプリケーション連携を設計する上で、前提となるものです。企業システムを開発する上でも非常に重要な要素なので、Java言語とは直接関係はありませんが、独立したレッスンとして用意しました。ただしHTTPの仕様やWeb APIの概念を理解している方は、このレッスンをスキップしていただいて問題ありません。

HTTPとは

HTTP(Hypertext Transfer Protocol)は、WWW(Webページを閲覧すること)のための通信プロトコルで、TCP/IPのアプリケーション層に位置付けられます。今日ではWWWに留まらず、Web APIなど、アプリケーション連携のためのプロトコルとしても広く利用されています。
HTTPはリクエスト・レスポンス型のプロトコルで、クライアントがサーバーにリクエストを送信すると、処理結果がレスポンスとして返送されるまで待機します。またHTTPはいわゆるステートレスなプロトコルであり、リクエスト毎にサーバーとの接続・切断が行われます。
なおユーザーの状態や、複数のリクエストに跨る形で情報を維持するためには、クッキーと呼ばれる仕組みを利用する必要があります。

URLとドメイン

HTTPではインターネット上に配置されたWebページ、データやサービスのことを、リソースと呼称します。そしてリソースの配置場所は、URL(Uniform Resource Locator)によって表します。
URLは「スキーム名://ホスト名.ドメイン名/パス」という記法で表されます。例えばGoogleのホームページを表すURLは"https://www.google.co.jp"になります。ここでは"https"がスキーム、すなわちプロトコルを表し、"google.co.jp"がドメイン名、"www"がホスト名になります。つまりこのURLは、Googleのホームページは、ドメイン"google.co.jp"のホスト"www"に配置されており、そのリソースに対してhttpsというプロトコルでアクセスする、ということを意味します。
URLの構成要素であるドメイン名とは、リソースの位置を表すアドレスのような概念です。ドメイン名は実世界の住所のように階層構造になっており、各階層の識別名をドットで区切って表記します。並び順は、一番右が最上位、一番左が最下位になります。
ドメイン名は、人間が理解しやすい抽象的な表記ですが、インターネット上における実際のアドレスはIPアドレスのため、通信するためにはIPアドレスが必要です。本レッスンでは取り上げませんが、IPアドレスはドメイン名およびホスト名から、名前解決という仕組みによって求めることが可能です。
なおURLの上位概念に、URI(Uniform Resource Identifier)があります。JavaでHTTP連携を実装する場合、URLの代わりにURIという用語が出てくることがありますが、基本的には、URL≒URIと考えて差し支えはありません。

HTTPのメッセージ構成

HTTPでは、リクエストやレスポンスとして送受信されるデータを、メッセージと呼びます。
リクエストのメッセージは、以下のような構成になります。

リクエストライン … HTTPメソッド+URL+HTTPバージョン
HTTPヘッダ
改行コード
ボディ

一方レスポンスのメッセージは、以下のような構成になります。

ステータスライン … HTTPバージョン+ステータスコード+ステータス
HTTPヘッダ
改行コード
ボディ

この中で、まずHTTPヘッダとは当該メッセージに関する付帯的な情報を表すもので「キー:値」の形式で設定されます。
またボディとは、メッセージの本体です。WWWの場合は、レスポンスのボディにはWebサーバー上に配置されたHTML文書がセットされます。またアプリケーション連携の場合は、ボディにはクライアント・サーバー間で交換する業務的な情報がセットされます。
それではここで、HTTPメッセージのサンプルを紹介します。例えばHTTPクライアントから、GETメソッドでHTTPサーバーを呼び出す場合は、以下のようなメッセージになります。

リクエスト

GET /?name=Alice HTTP/1.1
Content-Length: 0
Host: 127.0.0.1:8888
User-Agent: Java SE HttpClient

レスポンス

HTTP/1.1 200 OK
Date: Thu, 03 Nov 2022 01:16:01 GMT
Content-type: application/text
Content-length: 27

Hello! 私はAliceです。

メッセージボディとMIMEタイプ

HTTPでは、メッセージボディの種類や形式(フォーマット)をMIMEタイプによって表します。MIMEタイプは、リクエストメッセージ、レスポンスメッセージ、それぞれのContent-typeヘッダに設定します。
主要なMIMEタイプには、以下があります。

区分 MIMEタイプ データの種別
テキスト text/plain 任意のテキスト形式
text/html HTMLコード
text/csv CSV形式
text/javascript JavaScriptファイル
text/css CSSファイル
application/x-www-form-urlencoded フォームURLエンコーデッド形式
application/xml XML形式
application/json JSON形式
バイナリ イメージ image/gif GIFファイル
image/jpeg JPEGファイル
アプリケーション application/vnd.ms-excel EXCELファイル
application/pdf PDFファイル
application/zip ZIPファイル
その他 application/octet-stream 任意のバイナリデータ
multipart/form-data マルチパートデータ

例えばPOSTメソッド(後述)では、リクエストメッセージのMIMEタイプとして、フォームURLエンコーデッド形式が比較的よく使われてきました。ただし昨今では、HTTPによるアプリケーション連携を行う場合、リクエスト、レスポンスに関わらず、ボディはJSONと呼ばれる形式で記述するケースが一般的です。JSON形式のデータは、JacksonなどのOSSライブラリを利用することにより、Javaオブジェクトと容易に相互変換することができます。
なお前項の例にもあったとおり、GETメソッドの場合、リクエストのボディは空になります。

HTTPメソッド

HTTPでは、クライアントがサーバーに対してリクエストを送信するとき、「URLで指定したリソースをどのように操作したいか」を表すために、HTTPメソッドを使います。
代表的なHTTPメソッドには、GET、PUT、DELETE、POST、PATCHがあり、これらは指定したリソースに対するCRUD操作[1]を表します。

  • GETメソッドは、リソースを取得するためのものです。GETメソッドの場合、「クエリ文字列」をURLの後ろに付与することで、パラメータを指定します。前述したようにGETでは、リクエストにボディはありません。
  • PUTメソッドは、リソースを置換(存在しなければ新規作成)するためのものです。
  • DELETEメソッドは、リソースを削除するためのものです。
  • POSTメソッドは、リソースを新規作成するためのものです。ただしPOSTは、汎用的な更新系のメソッドとして利用されることもあります。
  • PATCHメソッドは、リソースへの部分的な変更を行うためのものです。

HTTPステータス

HTTPでは、サーバーがクライアントにレスポンスを返送するとき、「処理の結果がどのようになったのか」を表すためにHTTPステータスを使います。
HTTPステータスは、3桁の数字からなるコードで大きく以下の5つに分類されます。

(1) 情報取得(100番台)
(2) 成功 (200番台)
(3) リダイレクト (300番台)
(4) クライアントエラー (400番台)
(5) サーバーエラー (500番台)

この中で代表的なものに、処理の成功を表す「200 OK」や、「リソースが見つからない」という意味の「404 Not Found」があります。
またリダイレクトとは、サーバーがクライアントに対して別のURLにリクエストを転送させたい場合に応答します。一般的にはWebサイトが引越した場合などに用いられますが、Webアプリケーションの中で特定のページに遷移させたいケースでも利用されます。

Web APIとは

Web APIとは、HTTPプロトコルによってネットワーク越しにWebサービスを呼び出すための、外部インターフェースのことです。Web APIは、何らかの機能をもったサービスが、それを利用するアプリケーションのために提供します。本チャプターで取り上げる「HTTPによるアプリケーション連携」とは、まさに「Web APIの提供と利用」のことを指しています。すなわちWeb APIを提供する側がHTTPサーバー、Web APIを利用する側がHTTPクライアントになります。Web APIは、企業システムの内部で利用されるケースや、インターネット上で外部公開されるケースもあります。
なおWeb APIのことを単にAPIと呼ぶこともありますが、本レッスンでは、主にJava SEのクラスライブラリがメソッドとして提供する機能のことをAPIと呼んでいますので、混同しないように注意してください。

RESTサービスとは

RESTサービスとは、通信プロトコルとしてHTTPを利用するアプリケーション連携方式の一種です。RESTサービスはWeb APIとして提供されるため、REST APIとも呼ばれます。
元来のRESTサービスは、HTTP本来の仕様(URL、HTTPメソッド、HTTPステータスなどの仕様)に厳格に従ったアプリケーション連携方式を指していました。これを「RESTful Webサービス」と呼ぶことがあります。昨今ではHTTP本来の仕様に厳格であるかどうかに関わらず、HTTPによる一般的なアプリケーション連携のことを、広くRESTサービスと呼ぶケースが多いでしょう。

HTTPによる通信の全体像

前項までで取り上げた、URL、HTTPメソッド、HTTPステータスといったHTTPの仕様を踏まえると、HTTP通信の全体像は、以下のようになります。

【図6-2-1】HTTPによる通信の全体像
image.png

この図にはHTTPクライアント、HTTPサーバーが登場しています。WWWの場合、HTTPクライアントはWebブラウザ、HTTPサーバーはWebサーバーになります。このレッスンではアプリケーション連携(Web API)がテーマなので、HTTPクライアントおよびHTTPサーバーは、Javaによって作成されたプログラムになります。
クライアント・サーバー間の通信手順ですが、まずHTTPクライアントはHTTPサーバーのURLを特定し、HTTPメソッドを指定した上でリクエストを送信します。HTTPサーバーは通常、常時起動された状態にあり、HTTPクライアントからのリクエストを待ち続けています。そしてHTTPクライアントからデータを受信すると、その内容に応じて処理を行います。処理が終わるとその結果に応じてHTTPステータスをセットし、レスポンスをHTTPクライアントに返送します。

6.2.2 JavaにおけるHTTPクライアント

JavaにおけるHTTP通信の概要

このレッスンからは、HTTPによるアプリケーション連携をJavaで実現するための方法について説明します。
JavaではHTTPサーバーとクライアント、双方のアプリケーションを実装するための仕組みが標準的に提供されています。このうちHTTPサーバーはJakarta EE(旧Java EE)として標準化されおり、具体的にはサーブレットやJAX-RSと呼ばれるAPIを利用して構築します。また昨今ではOSSのフレームワークであるSpringBootによって、HTTPサーバーを構築する事例も多くなっています[2]
一方HTTPクライアントとしての機能は、Java SEのクラスライブラリとして提供されるため、本コースの対象範囲に含まれます。このチャプターでは、HTTPクライアントの作成方法を取り上げます。

HTTPクライアントAPI

Javaでは、当初よりHTTPクライアントとしての機能が、Java SEのクラスライブラリとして提供されていました。具体的には以下のようなクラスです。

  • java.net.URLConnection
  • java.net.HttpURLConnection

ただし、これらのクラスは機能性の観点で十分とは言えず、特にWeb APIを呼び出すという用途では、必ずしも使い勝手の良いものではありませんでした。そのため実際のアプリケーション開発では、Apache HttpClientなどのOSSを利用するケースが一般的でした。
その後Java 11において、新たにHTTPクライアントAPIがサポートされました。HTTPクライアントAPIは、後発の強みを活かし、非同期呼び出しやHTTP2.0への対応など豊富な機能を有しています。またラムダ式も活用することで、洗練されたインタフェースを提供します。今後新たにHTTPクライアントとなるアプリケーションを開発する場合は、HTTPクライアントAPIは有力な選択肢の1つになるでしょう。

HTTPクライアントAPIとして提供されるクラス

HTTPクライアントAPIとして提供される主要なクラスには、以下のようなものがあります。

  • java.net.http.HttpClient … HTTPクライアントを表すクラス
  • java.net.http.HttpRequest … リクエストを表すクラス
  • java.net.http.HttpResponse … レスポンスを表すクラス
  • java.net.http.HttpHeaders … HTTPヘッダを表すクラス

それぞれのクラスには様々なAPIが定義されていますが、それらに関する詳細は「APIリファレンス」を参照してください。次のレッスン以降では、実際にHTTPクライアントを作成しながら、具体的なAPIの使用方法を説明します。

HTTPサーバーの構築

次のレッスン以降では、HTTPクライアントAPIの使用方法を説明していきますが、具体的にHTTPクライアントを動作させるためには、通信先となるHTTPサーバーが必要です。そこで本レッスンでは、JDKに付属する簡易的なHTTPサーバーのためのクラスを利用します。具体的には、com.sun.net.httpserver.HttpServerというクラスです。
作成するHTTPサーバーの処理内容としてはいたってシンプルです。HTTPクライアントからリクエストを受信したら、そこに格納されたnameという名前のパラメータを取得します。そして、そのパラメータに対して「Hello! 私は〇〇です。」という文字列を生成し、それをクライアントに返送する、というものです。
このHTTPサーバーはあくまでも学習用なので、コードの掲載は割愛します。本格的なHTTPのサーバーアプリケーションを構築する場合は、前述したようにJakarta EEやSpringBootなどを利用してください。

6.2.3 HTTPクライアントの作成方法(1):同期型

HTTPクライアントの全体構成

ここではHTTPクライアントAPIを利用して、HTTPクライアントとなるプログラムを作成します。HTTPクライアントからHTTPサーバーの呼び出し方式には、同期型と非同期型の二通りがありますが、このレッスンではまず同期型から取り上げます。
HTTPクライアントはメインクラスとして作成します。具体的には、以下のようなコードになります。

pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync
public class Main_GET_Sync {
    public static void main(String[] args) throws Exception {
        // 手順1:HttpClientオブジェクトを生成する
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)
                .build();
        // 手順2:HttpRequestオブジェクトを生成する
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080?name=Alice"))
                .header("User-Agent", "Java SE HttpClient")
                .build();
        // 手順3:HttpRequestを送信し、HttpResponseを受け取る
        HttpResponse<String> response =
                client.send(request, HttpResponse.BodyHandlers.ofString());
        // 手順4:受け取ったHttpResponseに対して、何らかの後処理を行う
        int status = response.statusCode(); // ステータス
        System.out.println(status);
        HttpHeaders resHeaders = response.headers(); // HTTPヘッダ
        for (Map.Entry<String, List<String>> entry : resHeaders.map().entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue().get(0));
        }
        String resBody = response.body(); // ボディ
        System.out.println(resBody);
    }
}

同期型呼び出しの場合、処理の流れは、大まかに以下のような手順になります。
手順1:HttpClientオブジェクトを生成する
手順2:HttpRequestオブジェクトを生成する
手順3:HttpRequestを送信し、HttpResponseを受け取る(ブロッキングあり)
手順4:受け取ったHttpResponseに対して、何らかの後処理を行う
これらの一連の手順が、HTTPクライアント開発における基本的なテンプレートになります。この後、これらの各手順について順番に説明していきます。

手順1:HttpClientオブジェクトの生成

最初にHttpClientオブジェクトを生成します。HttpClientオブジェクトは、コンストラクタではなく、Buiderパターンと呼ばれるデザインパターンで生成します。既出のコードから、該当箇所を以下に示します。

snippet (pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync)
HttpClient client = HttpClient.newBuilder() //【1】
        .version(HttpClient.Version.HTTP_2) //【2】
        .build(); //【3】

まずnewBuilder()メソッドを呼び出し【1】、いくつかの属性を設定し【2】、最後にbuild()メソッドを呼び出す【3】、という流れです。設定可能な属性にはいくつかの種類がありますが、ここではHTTPのバージョンを2.0に設定しています。

手順2:HttpRequestオブジェクトの生成

次にHttpRequestオブジェクトを生成します。HttpRequestもHttpClientと同じように、Buiderパターンによってオブジェクトを生成します。既出のコードから、該当箇所を以下に示します。

snippet (pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync)
HttpRequest request = HttpRequest.newBuilder() //【4】
        .uri(URI.create("http://localhost:8080?name=Alice")) //【5】
        .header("User-Agent", "Java SE HttpClient") //【6】
        .build(); //【7】

まずnewBuilder()メソッドを呼び出し【4】、いくつかの属性を設定し【5、6】、最後にbuild()メソッドを呼び出す【7】、という流れは、HttpClientとまったく同じです。設定可能な属性にはいくつかの種類がありますが、設定が必須なのは宛先サーバーのURLです。URLを設定するためには、uri()メソッドを呼び出し、java.net.URIクラスによってURLを指定します【5】。ここではクエリ文字列として「name=Alice」を指定しています。またheader()メソッドによって、任意のHTTPヘッダを追加することができます【6】。ここでは"Java SE HttpClient"という値を持つ、User-Agentヘッダを追加しています。なおHTTPメソッドはデフォルトでGETに設定されますが、【4】以降【7】以前の任意の位置で.GET()を挟むことで、GETメソッドであることを明示することも可能です。

手順3:HttpRequestの送信とHttpResponseの取得

次に生成したHttpRequestをHTTPサーバーに送信します。そのためには、以下のようにHttpClientのsend()メソッドを呼び出します。

snippet (pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync)
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());

このメソッドは、HTTPサーバーを同期で呼び出すためのものです。従ってリクエストがサーバーに送信されると、レスポンスが返送されるまで待機します。サーバーからレスポンスが返送されると、レスポンスを表すHttpResponse<T>が戻り値として返されます。
このメソッドには引数が2つあります。まず第一引数には、すでに生成済みのHttpRequestを指定します。また第二引数には、HttpResponse.BodyHandler<T>のオブジェクトを指定します。HttpResponse.BodyHandlerのオブジェクトは、HttpResponse.BodyHandlersクラスのファクトリメソッドによって生成します。このクラスには「レスポンスのボディをどのような型で受け取りたいか」に応じて、様々なファクトリメソッドが用意されています。例えばレスポンスのボディを文字列として受け取りたい場合は、上記コードのようにofString()メソッド呼び出しを行います。HttpResponse.BodyHandlersには、その他にも、レスポンスのボディをバイト配列として受け取るためのofByteArray()メソッドや、ファイルとして受け取るためのofFile()メソッドなどのファクトリメソッドがあります。
さてsend()メソッドの戻り値はHttpResponse<T>ですが、型パラメータTは、HttpResponse.BodyHandlerの型パラメータと連動しています。このコードのように、HttpResponse.BodyHandlersのofString()メソッドを指定している場合は、HttpResponse.BodyHandlerの型パラメータがString型に決まるため、send()メソッドの戻り値もHttpResponse<String>型になります。

手順4:HttpResponseに対する後処理

最後にsend()メソッドによって返されたHttpResponseに対して、何らかの後処理を行います。既出のコードから、該当箇所を以下に示します。

snippet (pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync)
int status = response.statusCode(); //【8】ステータス
System.out.println(status);
HttpHeaders resHeaders = response.headers(); //【9】HTTPヘッダ
for (Map.Entry<String, List<String>> entry : resHeaders.map().entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue().get(0));
}
String resBody = response.body(); //【10】ボディ
System.out.println(resBody);

このコードでは、まずHttpResponseのstatusCode()メソッドにより、ステータスを取得します【8】。また同じくheaders()メソッドにより、java.net.http.HttpHeadersクラスとして、HTTPヘッダを取得します【9】。そしてそれらの内容を、コンソールに表示しています。最後にHttpResponseのbody()メソッドにより、ボディを取得します。ボディの型は、HttpResponse<T>の型パラメータTに対応しますが、このコードではT=String型に決まっているため、String型として受け取ります【10】。

POSTメソッドによるHTTP通信

前項まではHTTPメソッドとしてGETを選択していましたが、ここではPOSTメソッドによるHTTP通信の方法について説明します。POSTメソッドの場合は、手順2、すなわち「HttpRequestオブジェクトの生成」のためのコードが、以下のようになります。

snippet (pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_POST_Sync)
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080"))
        .header("Content-Type", "application/x-www-form-urlencoded") //【1】
        .header("User-Agent", "Java SE HttpClient")
        .POST(HttpRequest.BodyPublishers.ofString("name=Alice")) //【2】
        .build();

HttpRequestオブジェクトを生成する手順において、まずheader()メソッドによってHTTPヘッダを設定します。ここでは、ボディのMIMEタイプを、フォームURLエンコーデッド形式に設定します。そのためメソッドの第一引数に"Content-Type"を、第二引数として"application/x-www-form-urlencoded"を指定します【1】。前述したようにフォームURLエンコーデッド形式は、POSTメソッド使用時のボディ部のMIMEタイプとして、よく使われるものです。
次にPOST()メソッドを呼び出すと、HTTPメソッドとしてPOSTが選択されます。POSTメソッドには、HttpRequest.BodyPublisherのオブジェクトを指定します【2】。HttpRequest.BodyPublisherのオブジェクトは、HttpRequest.BodyPublishersクラスのファクトリメソッドによって生成します。このクラスには「リクエストのボディをどのような型で設定したいか」に応じて、様々なファクトリメソッドが用意されています。例えば、リクエストのボディを文字列として設定したい場合は、上記コードのようにofString()メソッド呼び出し、文字列としてボディを設定します。ここではコンテントタイプがフォームURLエンコーデッド形式なので、文字列"name=Alice"をボディに設定しています。

URLエンコードの必要性

このレッスンの例では、GETやPOSTの例で、クエリ文字列やボディに"name=Alice"という文字列を指定しましたが、2バイト文字を指定する場合はURLエンコードが必要です。
URLエンコードを行うためには、java.net.URLEncoderクラスのencode()メソッドを使います。具体的には、以下のようにエンコード対象の文字列と文字コードを指定します。

snippet
String encodedName = URLEncoder.encode("さいとう", "UTF-8");

すると"さいとう"という文字列が、以下のようにエンコードされます。

%E3%81%95%E3%81%84%E3%81%A8%E3%81%86

なおApache HttpClientなどのOSSには、URLエンコードされたパラメータを効率的に生成するためのAPIが用意されていますが、残念ながらJava SEのHTTPクライアントAPIは、この機能を持ちません。そのためクエリ文字列や、フォームURLエンコーデッド形式のボディは、開発者自身で構築しなければなりません。ただしPOSTメソッドのボディに関しては昨今ではJSON形式が主流のため、URLエンコードを自前で行うケースは、減りつつあると思います。

6.2.4 HTTPクライアントの作成方法(2):非同期型

HTTPクライアントからの非同期呼び出し

このレッスンでは、HTTPクライアントからHTTPサーバーへの非同期呼び出しの方法を説明します。非同期呼び出しを利用すると、リクエスト送信後、レスポンスを受け取るまでの間、待機が発生しません。そのためHTTP通信以外の他の処理を並行して実行したり、複数のHTTPサーバーに対して同時に呼び出しをしたりすることで、処理を効率化することができます。まずは早速、コードから見ていきましょう。

pro.kensait.java.advanced.lsn_6_2_4.httpclient.Main_GET_Async_1
public class Main_GET_Async_1 {
    public static void main(String[] args) throws Exception {
        // 手順1:HttpClientオブジェクトを生成する
        HttpClient client = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_2)
                .build();
        // 手順2:HttpRequestオブジェクトを生成する
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080?name=Alice"))
                .header("User-Agent", "Java SE HttpClient")
                .build();
        // 手順3:HttpRequestを送信し、HTTPサーバーを非同期で呼び出す
        CompletableFuture<HttpResponse<String>> future =
                client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
        // 手順4:後から何らかの方法でHttpResponseを取得し、何らかの後処理を行う
        HttpResponse<String> response = future.get();
        String resBody = response.body();
        System.out.println(resBody);
    }
}

非同期型呼び出しの場合の手順は、以下のようになります。
手順1:HttpClientオブジェクトを生成する
手順2:HttpRequestオブジェクトを生成する
手順3:HttpRequestを送信し、HTTPサーバーを非同期で呼び出す(ブロッキングなし)
手順4:後から何らかの方法でHttpResponseを取得し、何らかの後処理を行う
このうち手順1と手順2は同期型とまったく同じであり、非同期ならではなのは手順3と手順4になります。この後、これらの各手順について順番に説明していきます。

HTTPサーバーの非同期呼び出しとレスポンスの取得

ここでは、前項における手順3と手順4を具体的に説明します。まずは手順3、すなわち「HttpRequestを送信し、HTTPサーバーを非同期で呼び出す」手順を見ていきます。非同期呼び出しでは、生成したHttpRequestを、HttpClientのsendAsync()メソッドによって送信します。既出のコードから該当箇所を以下に示します。

snippet (pro.kensait.java.advanced.lsn_6_2_4.httpclient.Main_GET_Async_1)
CompletableFuture<HttpResponse<String>> future =
        client.sendAsync(request, HttpResponse.BodyHandlers.ofString());

このようにすると、暗黙的にスレッドが生成され、HTTPサーバーが非同期で呼び出されます。このメソッド呼び出しではブロッキングはなされず、直ちに戻り値が返ります。sendAsync()メソッドの第一引数、第二引数は、既出のsend()メソッドと同様です。

続いて手順4、すなわち「後から何らかの方法でHttpResponseを取得し、何らかの後処理を行う」手順です。
sendAsync()メソッドは、戻り値としてCompletableFuture<HttpResponse<T>>型を返します。java.util.concurrent.CompletableFutureクラスは、Futureインタフェースを実装したクラスの一種です。単なるFutureインタフェースに留まらず、非同期呼び出しの結果に対して効率的に後処理を行う様々な機能を提供しますが、それについては次項で詳しく説明します。
ここではシンプルに、Futureインタフェースのget()メソッドにより、HttpResponseを取得するものとします。既出のコードから、該当箇所を以下に示します。

snippet (pro.kensait.java.advanced.lsn_6_2_4.httpclient.Main_GET_Async_1)
HttpResponse<String> response = future.get();
String resBody = response.body();
System.out.println(resBody);

このように、get()メソッドによって後からHttpResponseを受け取り、レスポンスに対する必要な処理を施していきます。もちろん非同期呼び出しをしているので、手順3(リクエスト送信)と手順4(レスポンス取得)の間に、通信以外の何らかの別の処理を挟むことも可能です。

CompletableFutureによる後処理

ここでは前項における手順3と手順4に当たる処理を、CompletableFutureによって実装する方法を説明します。まず典型的なパターンとして、CompletableFutureクラスのthenAccept()メソッドを使ってみましょう。

snippet (pro.kensait.java.advanced.lsn_6_2_4.httpclient.Main_GET_Async_2)
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenAccept(response -> {
            String resBody = response.body();
            System.out.println(resBody);
        });

sendAsync()メソッドはCompletableFutureを返すので、それを変数に代入するのではなく、thenAccept()メソッドをチェーンさせます。thenAccept()メソッドには、このHTTPクライアントがレスポンスを受け取った後に実行する処理を、Consumerインタフェースとして渡すことができます。Consumerインタフェースは「引数1つ、戻り値なし」の関数型インタフェースであり、ラムダ式によって実装できるという点はチャプター4.1で説明したとおりです。ここではラムダ式に対してHttpResponseが引数として渡されるので、ボディを取り出し、それをコンソールに表示しています。

次に、CompletableFutureクラスの別のAPIとしてthenApply()メソッドがありますので、それを使ってみましょう。既出のコードを、以下のように書き換えます。

snippet (pro.kensait.java.advanced.lsn_6_2_4.httpclient.Main_GET_Async_3)
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
        .thenApply(response
                -> "### " + response.body() + " ###") //【1】
        .thenAccept(System.out::println); //【2】

thenApply()メソッドには、このHTTPクライアントがレスポンスを受け取った後に実行する処理を、Functionインタフェースとして渡すことができます【1】。Functionインタフェースは「引数1つ、戻り値1つ」の関数型インタフェースです。ここではラムダ式に対してHttpResponseが引数として渡されるので、ボディを文字列として取り出し、前後に"###"を付加するという加工を施しています。thenApply()メソッドは戻り値を返すので、さらに続けてthenAccept()メソッドをチェーンさせます【2】。ここではthenAccept()メソッドにメソッド参照を渡して、加工された文字列をコンソールに表示しています。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. HTTPの基本的な仕様(URL、HTTPメソッド、HTTPステータス)や、送受信されるメッセージの構成について。
  2. Web APIやRESTサービスの概念について。
  3. HTTPクライアントAPIの特徴や、主要なクラスの機能について。
  4. 同期型のHTTPクライアントの作成方法について。
  5. 非同期型のHTTPクライアントの作成方法やレスポンスの受け取り方について。
  6. 非同期型のHTTPクライアントにおける、CompletableFutureを使った後処理の実装方法について。
脚注
  1. CRUD操作とは、リレーショナルデータベースなどの永続的なデータストアにおいて、データを操作するための4つの操作、作成(Create)、取得(Read)、更新(Update)、削除(Delete)を表す用語。 ↩︎

  2. Jakarta EEやSpringBootについては、本コースの対象外になります。 ↩︎

Discussion