7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

6.2 HTTPによるアプリケーション連携~新Java Advanced編 / AI対応版

7
Last updated at Posted at 2023-02-04

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『新 Java Advanced編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Advanced編 / AI対応版』はこちらのリンクから購入できます。

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

  • Javaの基礎を学び終え、設計とレビューができるエンジニアを目指す方
  • AIが書いた並行処理や入出力のコードに潜むリスクを、仕組みから見抜けるようになりたい方
  • フレームワークの裏で使われるリフレクションやアノテーションの仕組みを理解したい方
  • 「Oracle認定Javaプログラマ」Goldの範囲を、仕組みから理解したい方

AIとチャットできるオールインワンの学習環境をリリースしました

Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。

Codinableの画面構成

学習に必要なものは、すべてCodinableの中で完結します。

  • 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
  • インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
  • 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
  • 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます

AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。

AIチャットに書き換えを指示し、差分を確かめて実行する

AIチャットは、受講者ご自身が生成AIサービス(Anthropic、OpenAI、Googleのいずれか)のAPIキーを登録して使うBYOK(Bring Your Own Key)方式です。利用料金は各社から直接請求されます。APIキーを登録しなくても、AIチャット以外の機能はすべて使えます。

Codinableのインストーラは、以下のGitHubのReleasesページからダウンロードできます(64ビット版のWindows 10/11、管理者権限は不要)。

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

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

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

チャプターの概要

チャプター6.1では、TCPとUDPでデータを直接送受信しました。本チャプターでは、その上のアプリケーション層のプロトコルであるHTTPを使い、アプリケーション同士を連携させる仕組みを取り上げます。HTTPのメッセージとステータスの意味を整理した後、Java 11で標準になったHTTPクライアントAPI(java.net.http)の構造と、同期・非同期の呼び出し方、タイムアウトの設定を確認します。

他のシステムのWeb APIを呼び出すコードは、AIが最もよく書くコードの1つです。そして、相手が遅い・止まっている・エラーを返すといった状況への備えが、最も抜けやすいコードでもあります。本チャプターでは、HTTPクライアントのコードを「失敗したときにどう振る舞うか」という観点でレビューできるようにします。非同期の呼び出しでは、チャプター3.2のFutureとチャプター4.1のラムダ式を使います。

6.2.1 HTTPのメッセージとWeb API

HTTPクライアントのコードを読むには、送っているリクエストと、受け取るレスポンスの構造が頭の中で再現できる必要があります。本レッスンでは、メッセージの構成と、ステータスコードに応じたクライアントの判断を整理します。

リクエストとレスポンスの構成

HTTPは、クライアントがリクエストを送り、サーバーがレスポンスを返すリクエスト・レスポンス型のプロトコルです。サーバーはリクエストの間でクライアントの状態を保持しない(ステートレス)ため、1つ1つのリクエストは独立して処理されます。HTTPでは、Webページやデータなどの取得・操作の対象をリソースと呼び、その場所をURL(例: http://localhost:8080/?name=Alice)で表します。Javaでは、URLを上位概念のURI(java.net.URI)として扱います。

リクエストメッセージとレスポンスメッセージの構成を図で比べます。

HTTPのリクエストメッセージとレスポンスメッセージの構成

  • リクエストメッセージは、リクエストライン(HTTPメソッド+URL+HTTPバージョン)、HTTPヘッダー、空行、ボディの4つの部分から成る
  • レスポンスメッセージは、ステータスライン(HTTPバージョン+ステータスコード+ステータス)、HTTPヘッダー、空行、ボディの4つの部分から成る
  • HTTPヘッダーは「キー: 値」の形式であり、空行は改行コードである
  • ボディは、メッセージの本体である
  • クライアントがリクエストを送信し、サーバーがレスポンスを返送する

ボディの形式はContent-TypeヘッダーにMIMEタイプ(application/json、text/plain、application/x-www-form-urlencodedなど)で示します。現在のアプリケーション連携では、ボディをJSONで書くのが一般的です。

HTTPメソッドとWeb API

HTTPメソッドは、リソースをどう操作したいかを表します。何度繰り返しても結果が同じになる性質を冪等(べきとう)性と呼び、通信に失敗したときに同じリクエストを再送してよいかの判断に関わります。

【表】代表的なHTTPメソッド

HTTPメソッド 操作 冪等性
GET リソースを取得する(ボディは無く、パラメータはクエリ文字列で渡す) あり
PUT リソースを置き換える あり
DELETE リソースを削除する あり
POST リソースを新規作成する(汎用の更新にも使われる) なし
PATCH リソースを部分的に変更する 保証されない

HTTPによってネットワーク越しにサービスを呼び出す外部インタフェースをWeb APIと呼び、URL・HTTPメソッド・ステータスの仕様に沿って設計したものをRESTサービス(RESTful Webサービス)と呼びます。本チャプターで扱う「アプリケーション連携」とは、Web APIの提供と利用のことで、提供する側がHTTPサーバー、利用する側がHTTPクライアントになります。

HTTPクライアントとHTTPサーバーの間の通信の流れを図で整理します。

HTTPクライアントとHTTPサーバーの通信の全体像

  • HTTPクライアントもHTTPサーバーも、Javaのプログラムである
  • リクエストは、URL(リソースの配置場所)、HTTPメソッド(リソースの操作)、ボディ(メッセージの本体)を持つ
  • HTTPサーバーは、リクエストを受けて業務処理を行う
  • レスポンスは、HTTPステータス(操作の結果)とボディ(メッセージの本体)を持つ

ステータスコードとクライアントの判断

HTTPステータスは3桁のコードで、先頭の数字で5つに分類されます。クライアントは、このコードを見て次の行動を決めます。

【表】HTTPステータスの分類

分類 コード 代表例
情報 100番台 100 Continue
成功 200番台 200 OK
リダイレクト 300番台 302 Found(別のURLへリクエストし直させる)
クライアントエラー 400番台 400 Bad Request、404 Not Found
サーバーエラー 500番台 500 Internal Server Error、503 Service Unavailable

レスポンスを受け取ったクライアントが、何を判断すべきかを図で示します。

レスポンスを受け取ったHTTPクライアントの判断の流れ

  • 起点は「レスポンスを受け取ったか」で、「いいえ(接続失敗・タイムアウト)」の場合は「冪等なメソッドなら回数と間隔を決めて再送、そうでなければエラーとして扱う」に進む
  • 「はい」の場合は、ステータスコードで3つに分かれる
  • 200番台は「ボディを処理する」に進む
  • 400番台は「リクエストの誤りなので、再送しても同じ結果。エラーとして扱う」に進む
  • 500番台は「サーバー側の問題。冪等なメソッドなら回数を限って再送を検討する」に進む

6.2.2 HTTPクライアントAPIと同期呼び出し

本レッスンでは、HTTPクライアントAPIを構成するクラスの役割と、同期呼び出しの流れを確認します。そのうえで、AIが書いたコードで最も抜けやすい、タイムアウトとステータスの確認を取り上げます。

HTTPクライアントAPIの構造

以前からあるHttpURLConnectionは使い勝手がよくなく、かつてはApache HttpClientなどのライブラリが使われていました。Java 11で標準化されたHTTPクライアントAPIは、HTTP/2や非同期呼び出しに対応し、ビルダーで組み立てる設計になっています。モジュールはjava.net.httpで、サンプルのmodule-info.javaではrequires java.net.http;を宣言しています。

主なクラスの関係を図で示します。

HTTPクライアントAPIを構成するクラスの関係

  • HttpClientは、接続の管理と送信を担い、HttpClient.newBuilder()で組み立てる。1つのインスタンスを複数のリクエストで使い回せる
  • HttpRequestは、URI・HTTPメソッド・ヘッダー・ボディ(BodyPublisher)を持つリクエストで、HttpRequest.newBuilder()で組み立てる
  • HttpClientのsend()(同期)またはsendAsync()(非同期)に、HttpRequestとBodyHandlerを渡す
  • BodyHandler(BodyHandlers.ofString()など)が、受信したボディをStringなどの型に変換する
  • 結果としてHttpResponse<T>が返り、statusCode()・headers()・body()でステータス・ヘッダー・ボディを取り出す

同期呼び出しの流れ

通信先には、JDKに付属する簡易HTTPサーバー(com.sun.net.httpserver.HttpServer)で作った学習用のサーバーlsn_6_2_3.httpserver.Mainを使います。ポート8080で待ち受け、パラメータnameを受け取って「Hello! 私は〇〇です」を返します。本格的なHTTPサーバーは、Jakarta EEやSpring Bootで構築します(本コースの対象外)。

同期型のHTTPクライアントを以下に示します。

  • 06_network_io: pro.kensait.java.advanced.lsn_6_2_3.httpclient.Main_GET_Sync
HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2) // 【1】
        .build();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080?name=Alice")) // 【2】
        .header("User-Agent", "Java SE HttpClient")
        .GET()
        .build();
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString()); // 【3】
int status = response.statusCode(); // 【4】
String resBody = response.body();
  • 【1】優先するHTTPのバージョンをHTTP/2にします。サーバーが対応していなければHTTP/1.1で通信します。
  • 【2】宛先のURLを指定します。クエリ文字列でname=Aliceを渡しています。
  • 【3】リクエストを送り、レスポンスが返るまで待ちます(ブロッキング)。ofString()を渡したため、戻り値はHttpResponse<String>に決まります(型パラメータはチャプター7.1参照)。
  • 【4】ステータスコードとボディを取り出します。

send()の結果は、通信が成り立ったかどうかで2つの経路に分かれます。その分かれ方を図で示します。

send()の結果が分かれる2つの経路

  • 左端の呼び出し元のスレッドがclient.send(request, ofString())を呼び、レスポンスが返るまで止まる
  • 経路1「通信が成り立たない」(接続できない、応答が無い、途中で切れる)では、send()がIOException(タイムアウトならHttpTimeoutException)を送出する
  • 経路2「通信が成り立った」では、ステータスコードが200でも404でも500でも、send()は例外を送出せずにHttpResponseを返す
  • 経路2の先に「statusCode()で成功かどうかを判断するのは呼び出し元」というラベルがある

Main_GET_Syncを「▶ 実行」すると、相手のサーバーも一緒に起動し、ステータス200、ヘッダー、ボディ「Hello! 私はAliceです」が表示されます。POSTで送る場合は、.POST(HttpRequest.BodyPublishers.ofString("name=Alice"))でボディを渡し、Content-Typeヘッダーにボディの形式を指定します(Main_POST_Sync)。

タイムアウトとステータスの確認

HTTPクライアントAPIは、タイムアウトを指定しなければ、相手が応答するまで待ち続けます。また、send()が例外を送出するのは通信そのものに失敗したときだけで、404や500のレスポンスは「正常に受け取ったレスポンス」として返ります。どちらもコードに書かなければ何も起きないため、レビューで最も見落としやすい箇所です。

タイムアウトを設定する2つの箇所を図で示します。

HTTPクライアントAPIの2つのタイムアウト

  • 時間の流れに沿って「接続の確立」「リクエストの送信」「レスポンスの待ち」の3区間が並ぶ
  • 「接続の確立」の区間には、HttpClient.BuilderのconnectTimeout(Duration)が対応し、超えるとHttpConnectTimeoutExceptionになる
  • 「リクエストの送信」から「レスポンスの待ち」までの区間には、HttpRequest.Builderのtimeout(Duration)が対応し、超えるとHttpTimeoutExceptionになる
  • 下部に「どちらも指定しなければ上限は無く、相手が応答するまで待ち続ける」という注記がある

タイムアウトとステータスの確認を加えた同期呼び出しの例です。

HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3)) // 【1】
        .build();
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://localhost:8080?name="
                + URLEncoder.encode("さいとう", StandardCharsets.UTF_8))) // 【2】
        .timeout(Duration.ofSeconds(5)) // 【3】
        .build();
HttpResponse<String> response =
        client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { // 【4】
    throw new IllegalStateException("HTTPステータス: " + response.statusCode());
}
  • 【1】接続の確立を3秒で打ち切ります。
  • 【2】日本語や記号をクエリ文字列に入れるときは、URLEncoder.encode()でURLエンコードします。文字列を連結するだけでは、URLとして正しくなりません(演習のMain_GET_Sync_Encoded)。
  • 【3】レスポンスが5秒以内に返らなければHttpTimeoutExceptionになります。
  • 【4】ステータスコードを確かめ、成功以外を呼び出し元に分かる形で知らせます。

HttpClientは内部に接続やスレッドを抱えるため、リクエストのたびに生成せず、1つを使い回します。Java 21以降はAutoCloseableを実装しており、不要になったらclose()で閉じられます。

レビューの観点

  • connectTimeout()とtimeout()が設定されているか。値は、呼び出し元の待てる時間から決められているか
  • statusCode()を確かめずにbody()を使っていないか。404や500のボディを正常なデータとして処理していないか
  • HttpClientをリクエストのたびに生成していないか
  • クエリ文字列やフォームのボディに、利用者の入力をURLEncoder.encode()せずに連結していないか
  • 通信失敗やタイムアウトで再送する場合、そのHTTPメソッドが冪等か、回数と間隔の上限があるかを確かめる

6.2.3 非同期呼び出しとCompletableFuture

非同期呼び出しでは、レスポンスを待たずに次の処理へ進めます。本レッスンでは、どのスレッドが何を待つのかと、非同期の処理で例外がどこへ行くのかを確認します。

同期呼び出しと非同期呼び出しの違い

sendAsync()は、リクエストを送るとすぐにCompletableFuture<HttpResponse<T>>を返し、レスポンスは別のスレッドで受け取ります。呼び出し元は、その間に別の処理を進めたり、複数のサーバーを同時に呼び出したりできます。

同期型と非同期型で、クライアントの処理がどこで待つかを図で比べます。

同期型と非同期型のHTTP呼び出しの比較

  • 同期型のsend()では、リクエスト送信の後、レスポンスが返るまで待機(ブロッキング)し、その後に後処理を行う
  • 非同期型のsendAsync()では、リクエスト送信の直後にCompletableFutureが返る
  • 非同期型のHTTPクライアントは、レスポンスを待たずに別の処理を進める
  • レスポンスは別スレッドで受信され、受信後にget()またはthenAccept()で後処理を行う

CompletableFutureによる後処理の連鎖

CompletableFutureは、Future(チャプター3.2参照)を実装したクラスで、結果が出たときに実行する処理を、ラムダ式でつなげて登録できます。thenApply()は値を加工する処理(Function)を、thenAccept()は値を受け取って使う処理(Consumer)を登録します。

レスポンスを加工してから表示する例を以下に示します。

  • 06_network_io: 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】
  • 【1】レスポンスを受け取ったら、ボディの前後に"###"を付けます。thenApply()は加工した値を持つ新しいCompletableFutureを返します。
  • 【2】加工が終わったら、その文字列を表示します。

このレッスンのサーバー(lsn_6_2_4.httpserver.Main)は、レスポンスを返す前に10秒待ちます。演習のMain_GET_Async_2は、レスポンスが返る前にmainメソッドが終わらないよう、最後にThread.sleep(30000)で待っています。完了を待つなら、本来は最後のCompletableFutureのjoin()や、上限を指定したget(timeout, unit)を使います。

後処理の連鎖と、途中で例外が起きた場合の流れを図で示します。

CompletableFutureの後処理の連鎖と例外の流れ

  • 上段の正常な流れは、「sendAsync()」→「thenApply()(加工)」→「thenAccept()(表示)」の順に、レスポンスの値が矢印で受け渡される
  • 下段の例外の流れでは、「sendAsync()」で接続に失敗すると、thenApply()とthenAccept()は実行されずに飛ばされる
  • 飛ばされた例外は、連鎖の末尾のexceptionally()(例外を受け取って代わりの値を返す)か、join()・get()で完了を待った呼び出し元に届く
  • 末尾にexceptionally()もjoin()も無い場合、例外はどこにも表示されずに消える

レビューの観点

  • 非同期の連鎖の末尾で、例外をexceptionally()などで扱うか、join()・get()で呼び出し元に届けているか(例外が黙って消えていないか)
  • 完了を待つのにThread.sleep()を使っていないか。get()に上限の時間を指定しているか
  • 非同期にしても、リクエストのtimeout()は必要であることを理解しているか(CompletableFutureがいつまでも完了しないことがある)
  • thenApply()などに登録した処理の中で、長時間ブロッキングする処理を行っていないか

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

本チャプターでは、以下を取り上げました。

  1. HTTPのメッセージはリクエストライン(またはステータスライン)・ヘッダー・空行・ボディから成り、HTTPメソッドの冪等性とステータスコードの分類が、クライアントが再送やエラーを判断する基準になること。
  2. HTTPクライアントAPIは、HttpClient・HttpRequest・BodyHandler・HttpResponseの役割が分かれており、HttpClientは使い回すものであること。
  3. タイムアウトは指定しなければ無制限であり、connectTimeout()とtimeout()の2か所で設定すること。また、send()は404や500でも例外を送出しないため、ステータスコードを確かめる必要があること。
  4. sendAsync()はCompletableFutureを返し、後処理を連鎖させられるが、例外を連鎖の末尾か呼び出し元で扱わないと、黙って消えること。
7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?