Micronautのフィルタ徹底解説!HTTPリクエスト共通処理をスマートに追加する方法
生徒
「MicronautでWebアプリを作っているのですが、全てのページでログインチェックをしたり、アクセスログを記録したりする共通の処理を、一つ一つのコントローラーに書くのが大変です。もっと楽な方法はありませんか?」
先生
「まさにそのような時に役立つのが『HTTPフィルタ』という機能です。リクエストがコントローラーに届く前や、レスポンスがブラウザに返される前に、横断的な処理を自動的に差し込むことができるんですよ。」
生徒
「なるほど!それを使えば、コードの重複を減らしてスッキリ管理できそうですね。具体的にどうやって作るんですか?」
先生
「アノテーションを一つ付けるだけで、特定のURLに対して共通処理を実行させることができます。基本から実際のコード例まで順番に解説していきますね!」
1. Micronautのフィルタ機能とは?
Webアプリケーションの開発において、認証、認可、ログ記録、レスポンスヘッダーの追加といった処理は、多くの画面やAPIで共通して必要になります。これらを全てのコントローラーに個別に記述すると、コードの保守が困難になり、バグの原因にもなります。Micronaut(マイクロノート)のフィルタ機能は、このような「横断的関心事」を一箇所に集約するための仕組みです。
フィルタは、リクエストがサーバーに届いてから目的の処理に渡るまでの「通り道」に設置する関所のようなものです。ここで不適切なリクエストを弾いたり、リクエストに情報を付け足したりすることができます。Micronautは非同期処理に強いフレームワークであるため、フィルタもノンブロッキングな設計になっており、高いパフォーマンスを維持したまま共通処理を実現できるのが大きな特徴です。
2. フィルタが動作するタイミングを理解する
フィルタには大きく分けて二つの役割があります。一つは、クライアントからのリクエストがコントローラーに届く前に実行される処理。もう一つは、コントローラーが処理を終えてレスポンスを返す際に実行される処理です。この流れを理解することで、適切なタイミングでロジックを組み込むことができるようになります。
例えば、アクセスログを記録したい場合は、リクエストが届いた瞬間の時間と、レスポンスが完了した瞬間の時間の両方を計測することで、処理にかかった時間を正確に把握できます。また、セキュリティチェックを行う場合は、コントローラーの重い処理が走る前にフィルタでリクエストを遮断することで、サーバーの負荷を軽減することも可能です。このように、フィルタはアプリケーションの入り口と出口をしっかり守る役割を担っています。
3. 基本的なフィルタの作り方とアノテーション
Micronautでフィルタを作成するには、HttpServerFilterインターフェースを実装したクラスを作成します。そして、そのクラスに@Filterアノテーションを付与するだけです。このアノテーションの引数には、フィルタを適用したいURLのパターンを指定します。例えば、「/api/**」と指定すれば、API関連の全てのアクセスに対して共通処理が適用されます。
インターフェースのdoFilterメソッドをオーバーライドすることで、具体的な処理を記述します。このメソッドの中で、次のフィルタや実際の処理を呼び出すためのFilterChainを実行するのが基本の形です。Javaの初心者の方でも、定型的な書き方を覚えてしまえば、非常に強力な武器になります。まずは、最もシンプルな「ログを出力するフィルタ」の例を見てみましょう。
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
@Filter("/hello/**")
public class LoggingFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
// リクエストが届いた時に実行される処理
System.out.println("リクエストを受け取りました: " + request.getPath());
// 次の処理へバトンタッチ
return chain.proceed(request);
}
}
4. レスポンスヘッダーを自動で追加する方法
Webサービスのセキュリティを向上させるため、全てのレスポンスに特定のHTTPヘッダー(例えば、キャッシュ制御やセキュリティポリシーに関するもの)を付加したい場合があります。これをフィルタで行うと非常にスマートです。chain.proceed(request)から返ってくる結果を加工することで、レスポンスの内容を動的に変更できます。
Micronautではリアクティブプログラミングの概念が使われているため、レスポンスの加工にはmapなどの操作を使います。一見難しそうに見えますが、決まった書き方をなぞるだけで大丈夫です。以下のコード例では、全てのレスポンスに「X-Custom-Header」という独自のヘッダーを自動的に追加しています。これにより、クライアント側に共通の情報を確実に伝えることができます。
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@Filter("/**")
public class HeaderFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
// chain.proceedを実行し、その結果(レスポンス)に処理を加える
return Flux.from(chain.proceed(request)).map(response -> {
// レスポンスヘッダーを追加
response.getHeaders().add("X-Custom-Header", "Micronaut-is-Awesome");
return response;
});
}
}
5. 特定のURLパターンにだけフィルタを適用する
全ての通信に対してフィルタをかけるのではなく、特定の範囲だけに限定したい場合がよくあります。例えば、ログインが必要な管理画面は「/admin/**」、公開されているAPIは「/api/public/**」といった具合です。@Filterアノテーションには複数のパスを指定したり、ワイルドカードを使用したりすることができます。
また、除外設定を組み合わせることで、「全てのページに適用するけれど、ログイン画面だけは除外する」といった柔軟な運用も可能です。これにより、不要な処理をスキップさせてパフォーマンスを最適化できます。URLの設計とフィルタの適用範囲を連動させることで、安全で効率的なルーティング構造を構築することができます。
6. 認証チェック機能をフィルタで実装する
フィルタの最も代表的な活用例が、認証・認可のチェックです。リクエストヘッダーに含まれる認証トークンを検証し、もし不正なトークンであれば、コントローラーに処理を渡す前に「401 Unauthorized」などのエラーレスポンスを返します。これにより、個別のビジネスロジックの中で認証コードを書く必要がなくなります。
もし認証に失敗した場合は、chain.proceed(request)を呼び出さずに、自分で作成したHttpResponse.unauthorized()を返すだけで、処理を中断させることができます。この「処理を先に進めるか、ここで止めるか」を制御できるのがフィルタの最大の強みです。セキュリティを強固にするための必須テクニックと言えるでしょう。
package com.example;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.filter.HttpServerFilter;
import io.micronaut.http.filter.ServerFilterChain;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Filter("/secure/**")
public class AuthFilter implements HttpServerFilter {
@Override
public Publisher<MutableHttpResponse<?>> doFilter(HttpRequest<?> request, ServerFilterChain chain) {
// ヘッダーから認証情報を取得
String authHeader = request.getHeaders().get("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// 認証情報がない場合は、ここでエラーを返して終了
return Mono.just(HttpResponse.unauthorized());
}
// 問題なければ次の処理へ
return chain.proceed(request);
}
}
7. フィルタの実行順序を制御する
アプリケーションに複数のフィルタが存在する場合、それらが実行される順番が重要になることがあります。例えば、「ログ記録フィルタ」は一番最初に動いてほしいですし、「認証フィルタ」はログの次、というように優先順位を決めたいケースです。MicronautではOrderedインターフェースを実装することで、実行順序を数値で指定できます。
数値が小さいほど優先順位が高く、先に実行されます。デフォルトでは順序は保証されませんが、この仕組みを使うことで、確実に意図した通りの順序で共通処理を積み重ねていくことができます。複雑なシステムになればなるほど、この実行順序の管理が安定稼働の鍵となります。プロフェッショナルな開発を目指すなら、ぜひ覚えておきたい知識です。
8. 非同期処理とリアクティブプログラミングの基礎
Micronautのフィルタを扱う上で避けて通れないのが、非同期処理の考え方です。コード例の中で登場したPublisherやFlux、Monoといった型は、Project Reactorなどのリアクティブストリームライブラリのものです。これらは「将来返ってくるデータ」を扱うための箱のようなものです。
従来のJavaアプリのように「処理が終わるまで待つ」のではなく、「終わったらこの処理をしてね」という予約を入れるようなイメージで記述します。最初は戸惑うかもしれませんが、この書き方のおかげで、サーバーは少ないメモリで大量のリクエストを同時に捌くことができるようになっています。フィルタを使いこなすことは、最新のJava開発のトレンドである非同期処理に慣れる絶好の機会でもあります。
9. フィルタ内でのエラーハンドリング
フィルタの処理中に例外が発生した場合の挙動も考慮しておく必要があります。フィルタ内で予期せぬエラーが起きたとき、適切なエラーレスポンスを返さないと、クライアントは接続が切れたり、原因不明のエラーに見舞われたりします。try-catchで囲む方法もありますが、リアクティブなストリームの中では専用のエラー処理用メソッドを使います。
例えば、onErrorResumeというメソッドを使うと、エラーが起きた時に代わりのレスポンスを出すように設定できます。これにより、共通処理の部分でトラブルが起きても、システム全体がクラッシュすることなく、安全にユーザーへエラーを通知できるようになります。堅牢なアプリケーションを作るためには、正常な時だけでなく異常な時の設計も不可欠です。
10. フィルタ導入による開発効率の劇的な向上
フィルタを導入することで、コントローラーのコードは驚くほどシンプルになります。各コントローラーは、自分が担当する「本来の業務ロジック」だけに集中でき、認証やログといった付随的な処理から解放されるからです。これは読みやすさの向上だけでなく、テストのしやすさにも繋がります。
共通処理を一箇所にまとめることで、将来的に「全てのAPIのログ形式を変えたい」とか「新しい認証方式を追加したい」といった変更が必要になった時も、フィルタを一箇所修正するだけで済みます。まさにプログラミングの格言である「DRY(Don't Repeat Yourself:同じことを繰り返さない)」を体現する機能です。Micronautを使いこなすなら、このフィルタ機能を自分のものにして、洗練されたコードを書きましょう!
package com.example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/hello")
public class HelloController {
@Get("/world")
public String index() {
// フィルタのおかげで、ここではビジネスロジックに集中できる!
return "共通処理はフィルタにお任せ!Hello World";
}
}