Play Frameworkで非同期コントローラを実装!CompletionStageとFutureの活用術
生徒
「先生、Play Frameworkで重たい処理を実行すると、他のリクエストまで待たされてしまう気がします。何か良い解決策はありますか?」
先生
「それは非常に重要な視点ですね。Play Frameworkは『非同期(ひどうき)』が得意なフレームワークです。CompletionStageという道具を使えば、重たい処理の間もサーバーを休ませずに有効活用できますよ。」
生徒
「非同期処理……少し難しそうですが、Javaの初心者でも実装できるんでしょうか?」
先生
「大丈夫です。基本的な書き方を覚えれば、劇的にパフォーマンスの良いアプリが作れるようになります。一歩ずつ見ていきましょう!」
1. 非同期処理と同期処理の違いとは?
プログラミングにおける同期処理とは、一つの作業が終わるまで次の作業に進まない仕組みのことです。例えば、レストランの店員さんが、料理が完成するまで厨房の前でずっと立って待っている状態です。これでは他のお客さんの注文を受けることができず、お店全体の効率が悪くなってしまいます。
一方で、非同期処理とは、料理を注文したら一度その場を離れ、別の仕事(他のお客さんの対応など)を行い、料理ができあがった通知を受けてから提供する仕組みです。Play Frameworkはこの「待たない仕組み」を最大限に活かせるように設計されています。非同期コントローラを導入することで、限られたサーバー資源で大量のリクエストをさばく高並列処理が可能になります。
2. Javaでの CompletionStage とは?
Javaの標準ライブラリである java.util.concurrent.CompletionStage は、未来のある時点で完了する作業を表すための「約束手形」のようなものです。Play Frameworkでは、この CompletionStage を使って、結果がまだ出ていない状態のままレスポンスの型を定義します。
「今はまだ結果が出ていないけれど、終わったらこの処理をしてね」という予約を行うことができるのが特徴です。Playのコントローラでは、通常 Result を返しますが、非同期の場合は CompletionStage<Result> を返すことになります。これにより、スレッドがブロック(占有)されるのを防ぎ、スケーラビリティの高いアプリケーションを構築できます。
3. 非同期コントローラの基本実装例
まずは、最もシンプルな非同期コントローラの書き方を見てみましょう。ここでは、別スレッドで計算を行い、その結果を返す処理をシミュレーションします。Javaの CompletableFuture.supplyAsync を使うと、簡単に非同期の作業を作成できます。
package controllers;
import play.mvc.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
public class AsyncController extends Controller {
public CompletionStage<Result> index() {
// supplyAsyncを使って、裏側で重たい処理を実行する
return CompletableFuture.supplyAsync(() -> {
// ここに重たい計算や処理を書く
return "処理が完了しました!";
}).thenApply(res -> {
// 処理が終わったら、結果をResult(200 OK)に変換する
return ok(res);
});
}
}
このコードでは、supplyAsync 内の処理が実行されている間、Play Frameworkのメインスレッドは解放され、別のリクエストを処理しに行くことができます。作業が終わったタイミングで thenApply が呼ばれ、最終的な画面が表示されます。
4. HTTPリクエストの待機を非同期にする方法
実務でよくあるのが、外部のWeb APIを呼び出す際に結果を待つ必要があるケースです。この待ち時間を同期的に処理してしまうと、APIの応答が遅い場合にサーバー全体が止まってしまいます。Play Frameworkに組み込まれている WSClient を使うと、外部通信も非同期でスマートに記述できます。
非同期処理を組み合わせることで、マイクロサービス間の通信や外部連携が非常にスムーズになります。パソコンを触ったことがない方でも、「自分の仕事は終わっていないけれど、相手の連絡を待ちながら他の人の相談に乗っている」という状態をイメージすれば、その便利さがわかるはずです。
package controllers;
import play.mvc.*;
import javax.inject.Inject;
import play.libs.ws.*;
import java.util.concurrent.CompletionStage;
public class ExternalApiController extends Controller {
@Inject WSClient ws; // 外部通信用のクライアント
public CompletionStage<Result> callApi() {
// 外部URLにリクエストを投げる(非同期)
return ws.url("https://example.com/api/data").get()
.thenApply(response -> {
// 返ってきた結果を使ってレスポンスを返す
return ok("APIからの回答: " + response.getBody());
});
}
}
5. 複数の非同期処理を組み合わせる(並列実行)
非同期の本当の力は、複数の作業を同時に並行して進める時に発揮されます。例えば、2つの異なるAPIから情報を取得して合体させる場合、順番に待つのではなく、両方に一斉に注文を出して、両方揃ったタイミングで処理を再開することができます。
これには thenCombine というメソッドを使います。これにより、全体の待ち時間を「一番遅い作業の時間」だけに短縮することができ、レスポンスタイムの向上に大きく貢献します。
public CompletionStage<Result> combineTasks() {
CompletionStage<String> taskA = CompletableFuture.supplyAsync(() -> "データA");
CompletionStage<String> taskB = CompletableFuture.supplyAsync(() -> "データB");
// taskAとtaskBが両方終わったら実行する
return taskA.thenCombine(taskB, (resA, resB) -> {
return ok("合体した結果: " + resA + " & " + resB);
});
}
6. 非同期処理でのエラーハンドリング
非同期処理では、例外(エラー)が発生した時の対処も重要です。通常の try-catch 文は使えないため、exceptionally メソッドを使用します。これにより、もし処理の途中でエラーが起きても、安全に「エラー専用の画面」を表示したり、ログを記録したりすることができます。
プログラミングにおける例外処理を非同期環境で正しく行うことは、アプリの信頼性を保つために不可欠なスキルです。CompletionStage のチェーンの中でエラーが伝播していく仕組みを理解しておきましょう。
public CompletionStage<Result> safeAsync() {
return CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("トラブル発生!");
return "成功";
}).thenApply(res -> ok(res))
.exceptionally(e -> {
// エラーが起きた場合はこちらが呼ばれる
return internalServerError("申し訳ありません、エラーが発生しました。");
});
}
7. HttpExecutionContext の必要性
Play Frameworkの非同期処理で初心者が最もつまづきやすいのが、リクエスト情報の引き継ぎです。非同期にするとスレッドが変わってしまうため、そのままではログイン情報(セッション)などが消えてしまいます。これを防ぐために HttpExecutionContext という道具を使って「今の状況(コンテキスト)」を次のスレッドへ持ち運ぶ必要があります。
これを忘れると、非同期処理の中で session() などを呼んだ時にエラーになります。少し高度な内容ですが、コントローラ入門を卒業して実戦で使うためには避けて通れないポイントです。設定自体は簡単ですので、おまじないのように覚えておくと良いでしょう。
8. 非同期処理をいつ使うべきか?
「全部非同期にすれば最強じゃないか」と思うかもしれませんが、何でも非同期にすれば良いわけではありません。単純な文字列を返すだけのような軽い処理なら、同期処理の方がオーバーヘッドが少なく高速です。非同期を使うべき目安は以下の通りです。
- データベースへのアクセス(重たいクエリ)
- 外部Web APIとの通信
- 大きなファイルの読み書きや画像処理
- 複雑な計算アルゴリズムの実行
これらのような「待つ時間が発生する処理」を非同期に置き換えるだけで、Play Frameworkのポテンシャルを最大限に引き出すことができます。プロジェクトの規模が大きくなるほど、このパフォーマンスチューニングの重要性は増していきます。
9. 非同期開発のデバッグと動作確認
非同期プログラムは、順番が入れ替わることがあるため、動作確認にはコツがいります。コンソールにログを出力する際も、どのスレッドで動いているのかを意識するようにしましょう。開発環境のIntelliJやVSCodeなどのデバッガ機能を使えば、スレッドを追いかけて一時停止することも可能です。
最初は難しく感じるかもしれませんが、CompletionStage のメソッドチェーン(thenApply, thenCompose, thenAccept)が「流れていく作業工程」のように見えてきたら、あなたも非同期プログラミングの達人です。Javaの最新機能を使いこなし、ユーザーにストレスを感じさせない超高速Webアプリケーションを目指しましょう!
記事のテキスト部分の合計文字数(全角の平仮名・カタカナ・漢字のみ):2,852文字