フィルタとインターセプタ
このレッスンでは、すべてのリクエストが必ず通る共通の通り道である Filter と HandlerInterceptor の違いを理解し、チェーンという考え方を身につけます。
全リクエストに共通する処理をどこに置くか
認証、ログ出力、文字コードの設定、処理時間の計測。こうした処理は、特定のコントローラだけでなくすべてのリクエストに必要です。かといって全ハンドラメソッドの先頭に同じ 3 行を書き写すのは現実的ではありません。リクエストが通る道の途中に、共通処理を差し込む場所が用意されています。
Spring MVC のリクエストは、大きく言うと次の順で流れます。
プレーンテキスト
クライアント
-> サーブレットコンテナ (Tomcat)
-> Filter チェーン
-> DispatcherServlet
-> HandlerInterceptor
-> Controller のハンドラメソッド
<- HandlerInterceptor
<- DispatcherServlet
<- Filter チェーン
<- クライアントFilter は外側、Interceptor は内側
この図が示すとおり、Filter と HandlerInterceptor の最大の違いは立ち位置です。
| Filter | HandlerInterceptor | |
|---|---|---|
| 仕様の出どころ | サーブレット仕様 (jakarta.servlet) | Spring MVC 独自 |
| 位置 | DispatcherServlet の外側 | DispatcherServlet の内側 |
| 対象 | そのアプリへ来るすべてのリクエスト | DispatcherServlet が扱うリクエスト |
| どのハンドラが呼ばれるかを知れるか | 知れない | 知れる |
| 向く用途 | 文字コード、認証、リクエスト本文の書き換え | ハンドラごとの権限確認、処理時間の計測 |
Filter はサーブレットの世界の道具なので、Spring がまだ何も判断していない段階で動きます。リクエストやレスポンスをラップして中身を差し替えられるのが強みです。一方 HandlerInterceptor は Spring MVC の中で動くので、preHandle の引数として「これからどのハンドラが呼ばれるのか」を受け取れます。ハンドラに付いた独自アノテーションを見て通す通さないを決める、といった芸当は Interceptor でなければできません。
Java
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(req, res); // 次へ渡す。これを呼ばないとここで止まる
long elapsed = System.currentTimeMillis() - start;
}
}チェーンは順に通り、途中で切れます
Filter は 1 つとは限らず、複数が数珠つなぎになります。ここで覚えるべき性質が 2 つあります。1 つは、登録した順に前から適用されるということ。もう 1 つは、chain.doFilter を呼ばずに応答を書いてしまえば、その先のフィルタもコントローラも一切呼ばれない、ということです。認証フィルタが未認証のリクエストを 401 で打ち切るのは、まさにこの性質を使っています。
順序が意味を持つ場面は多くあります。認証フィルタの前にログフィルタを置けば未認証のリクエストも記録できますが、後ろに置けば認証を通ったものしか記録されません。どちらが正しいかは目的次第で、だからこそ順序は意識して決める必要があります。Spring Boot では FilterRegistrationBean の setOrder や @Order で順序を指定できます。
演習で書くもの
Spring が FilterChain の中で行っている「前から順に適用し、拒否が出たらそこで打ち切る」という制御そのものを、自分で書きます。フィルタの並びを名前の列として受け取り、順に適用した結果を返してください。途中のフィルタが拒否したら、そこで打ち切って理由を返します。実際の Filter が chain.doFilter を呼ばずに応答を返す状況にあたります。
演習で使うひな形
applyFilter("upper", "hello") の結果は body が "HELLO"、rejection が null です。applyFilter("auth", "hello") では rejection が "rejected by auth" になります。順に呼び、拒否理由があればその場で返し、正常なら result.body を次のフィルタへ渡します。
要件
- 各名前に applyFilter を順番に適用し、次には前の加工結果を渡すこと
- rejection が null でなければ直ちに理由を返すこと
- 全フィルタを通ったら最後の本文を返し、空配列なら元の本文を返すこと