はじめに
筆者はSIerでエンジニアをしています。フロントエンドから入りました。Reactとかやっています。
最近SpringBootを扱っていますが、なんていうのでしょう。SpringBootというフレームワークの上に乗っているという便利さを感じています。
今回はその中でfilterについて触れます。
まずは一番やりたいこと
APIを作りたい。
@RestController
@RequestMapping("/api")
public class SampleApi {
@GetMapping("/hello")
public ResponseEntity<String> getHello() {
return ResponseEntity.ok("Hello, World!");
}
}
次にやりたいこと
もう一個APIを作りたくなる。
@RestController
@RequestMapping("/api")
public class SampleApi {
@GetMapping("/hello")
public ResponseEntity<String> getHello() {
return ResponseEntity.ok("Hello, World!");
}
@GetMapping("/thanks")
public ResponseEntity<String> getThanks() {
return ResponseEntity.ok("Thank you!");
}
}
さて、こちらをベースに考えていきましょう。
前処理、後処理が欲しい
開始、終了のログを出したくなりました。
SpringBootの機能を使用して、以下のように実装してみます。
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class SampleApi {
private static final Logger logger = LoggerFactory.getLogger(SampleApi.class);
@GetMapping("/hello")
public ResponseEntity<String> getHello() {
logger.info("start /api/hello");
ResponseEntity<String> response = ResponseEntity.ok("Hello, World!");
logger.info("end /api/hello");
return response;
}
@GetMapping("/thanks")
public ResponseEntity<String> getThanks() {
logger.info("start /api/thanks");
ResponseEntity<String> response = ResponseEntity.ok("Thank you!");
logger.info("end /api/thanks");
return response;
}
}
まあ、、、悪くないのですが、、、APIの数が100個になったら面倒ですね。
ここでフィルターの出番です。
フィルターについて
フィルターとは、リクエストとレスポンスの前後に共通の処理を挟み込める機能です。たとえば、上記のようなログ出力をすべてのAPIに適用したい場合に便利です。
具体的な実装例を見てみましょう。
@Component
public class LoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
logger.info("start {}", path);
try {
chain.doFilter(request, response);
} finally {
logger.info("end {}", path);
}
}
}
これにより、先ほど各APIメソッドに書いていたログ出力の処理を一箇所にまとめることができます。コントローラー側のコードはシンプルなまま、すべてのAPIリクエストに対して自動的にログ出力が行われるようになります。
あらためて、実装が以下のように、ログを一箇所に集約できました。
修正前
修正後
これにより、各APIでログ出力のコードを書く必要がなくなり、フィルター側で一括して処理できるようになったことが視覚的に分かります。APIが増えても、フィルターの実装は1箇所のままで済みます。
こんな使い道
さて、ログ出力以外にも、フィルターはいろんな場面で活躍します。
例えば
-
認証周り
- ヘッダーからJWTトークンを取り出して検証
- セッションの確認
- 権限チェック
-
パフォーマンス監視
@Component public class PerformanceFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(PerformanceFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long startTime = System.currentTimeMillis(); try { chain.doFilter(request, response); } finally { long endTime = System.currentTimeMillis(); long processTime = endTime - startTime; // 1秒以上かかってる処理を見つけたい if (processTime > 1000) { logger.warn("ちょっと重いAPIを検出: {}ms", processTime); } } } }
-
リクエスト/レスポンスの加工
- 文字コードの設定
- ヘッダーの追加
- レスポンスの圧縮
他にもこんなのをフィルターでまとめます、というケースがあればご教示ください。
まとめ
SpringBootのフィルターについて見てきました。
-
フィルターは共通処理を一箇所にまとめられる
- 処理の重複を避けられる
- コードの見通しが良くなる
- APIの増加に伴うメンテナンスコストを抑えられる
-
主な使い道
- ログ出力(今回の例)
- 認証・認可の処理
- パフォーマンス監視
- リクエスト/レスポンスの加工
それぞれのcontrollerに実装するのも間違えではありません。でも冗長であり、変更に弱いです。SpringBootのフィルターは、共通の処理をまとめるニーズにぴったりの機能です。
少しでもフィルターについて、理解が深まれば幸いです。
その他
なぜあの実装でフィルターになるか
実装のポイントを振り返ってみましょう
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
ここでポイントとなるのが3つです:
-
@Component というアノテーション
- これをつけると、SpringBootがコンポーネントと認識します
- アプリ起動時に自動的に組み込まれます
-
implements Filter
- これは「フィルターの機能を持つよ」という印です
- SpringBootは「フィルターだね、じゃあリクエストが来たら通すよ」と理解します
-
doFilter メソッド
- ここが実際の処理を書く場所
-
chain.doFilter()
を境に、前と後ろに処理が書けます - リクエストが来たら、上から順番に実行されます
シンプルですが、これだけの実装でフィルターとして機能します。
SpringBootが「フィルターの仕組み」を用意してくれているおかげですね。
さいごに
てことで、フレームワークのもとでの開発はとーーーっても便利ですね。
一つずつ理解を深めていきます。