Spring BootのInterceptorとFilterの違いと使い分け - リクエスト前後処理の実装パターン
Spring Bootでリクエストログや認証チェックを実装しようとすると、FilterとInterceptorのどちらを使うべきか迷いますよね。どちらも「リクエスト前後に共通処理を挟む」という目的は同じですが、実行タイミングや扱える情報が異なります。
この記事では、FilterとHandlerInterceptorの技術的な違いを整理し、認証・ログ・CORSなど実務でよくあるケースごとに適切な選択基準を示します。
FilterとInterceptorの基本的な役割
FilterはJavaのサーブレット仕様で定義された仕組みで、サーブレットコンテナレベルで動作します。一方、InterceptorはSpring MVC独自の仕組みで、Springのコンテキスト内で動作します。
どちらもリクエスト前後に共通処理を挟むための横断的関心事(クロスカッティング)の実装手段ですが、動作する層が違うため適切な使い分けが必要です。
実行タイミングの違い
FilterとInterceptorの最も重要な違いは実行タイミングです。
リクエスト → Filter → DispatcherServlet → Interceptor → Controller → Interceptor → DispatcherServlet → Filter → レスポンス
FilterはDispatcherServletより前に実行されるため、Spring MVCの処理が始まる前にリクエストを加工したり、Spring MVCに到達する前にリクエストを遮断できます。
InterceptorはDispatcherServletがリクエストを受け取った後、Controllerメソッドが呼ばれる直前・直後に実行されます。このためController固有の情報にアクセスできます。
Spring DIコンテナ管理の違い
InterceptorはSpring管理のBeanなので、@Autowired で他のBeanを簡単に注入できます。
@Component
public class LoggingInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService; // Bean注入が自然
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// userServiceを使った処理
return true;
}
}
Filterも @Component をつければBean注入できますが、本来はサーブレットコンテナが管理するものです。Spring BootではFilterを自動的にBean登録してくれますが、明示的に制御したい場合は FilterRegistrationBean を使います。
アクセス可能な情報の違い
Filterは HttpServletRequest と HttpServletResponse のみにアクセスできます。
Interceptorは HandlerMethod オブジェクトにアクセスできるため、呼び出されるControllerメソッドの情報を取得できます。アノテーション情報やメソッドパラメータの型情報などが扱えるため、より柔軟な処理が可能です。
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// メソッドのアノテーション情報を取得
if (method.isAnnotationPresent(RequireAuth.class)) {
// 認証チェック処理
}
}
return true;
}
Filter実装の基本パターン
Filterを実装する際は OncePerRequestFilter を継承するのが一般的です。これはリクエストごとに一度だけ実行されることを保証します。
@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
try {
filterChain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.info("{} {} - {}ms", request.getMethod(), request.getRequestURI(), duration);
}
}
}
@Order アノテーションで実行順序を制御できます。数値が小さいほど先に実行されます。
Interceptor実装の基本パターン
Interceptorは HandlerInterceptor インターフェースを実装します。
@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
long startTime = (Long) request.getAttribute("startTime");
long duration = System.currentTimeMillis() - startTime;
logger.info("Controller実行時間: {}ms", duration);
}
}
Interceptorを登録するには WebMvcConfigurer を実装します。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ExecutionTimeInterceptor executionTimeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(executionTimeInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/public/**");
}
}
addPathPatterns と excludePathPatterns でInterceptorを適用するURL範囲を制御できます。
リクエスト/レスポンスログの実装
全リクエストのログを取りたい場合はFilterを使います。DispatcherServletに到達しなかったリクエスト(404など)も記録できます。
Controllerメソッド単位でログを取りたい場合はInterceptorが適しています。どのControllerメソッドが呼ばれたかの情報を記録できます。
リクエストボディやレスポンスボディをログに出力したい場合は、ContentCachingRequestWrapper と ContentCachingResponseWrapper を使います。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestBodyLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
byte[] requestBody = wrappedRequest.getContentAsByteArray();
byte[] responseBody = wrappedResponse.getContentAsByteArray();
// ログ出力処理
wrappedResponse.copyBodyToResponse(); // 重要: レスポンスをコピーして返す
}
}
詳細なログ設定については Spring Bootのロギング設定ガイド を参照してください。
認証チェックの実装
Spring Securityを使っている場合は、認証処理はSecurityFilterChainに任せるのが基本です。
独自のトークン認証を実装する場合で、Spring Securityを使わないならFilterで実装します。すべてのリクエストに対して認証チェックが必要なためです。
Controller呼び出し前に特定のアノテーションをチェックして権限確認したい場合はInterceptorが便利です。
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireAdmin annotation = handlerMethod.getMethodAnnotation(RequireAdmin.class);
if (annotation != null) {
// 管理者権限チェック
if (!isAdmin(request)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
}
}
return true;
}
Spring Securityの基本的な使い方は Spring BootでBasic認証を実装するチュートリアル で解説しています。
CORS設定の実装
CORS設定はSpringが提供する標準的な方法を使うべきです。Filterで実装するのは避けましょう。
最も簡単なのは @CrossOrigin アノテーションです。
@RestController
@CrossOrigin(origins = "http://localhost:3000")
public class ApiController {
// ...
}
アプリケーション全体で設定する場合は WebMvcConfigurer を使います。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
}
例外ハンドリングの実装
Controller内で発生した例外は @ControllerAdvice で処理するのが基本です。Interceptorで例外を検知したい場合は afterCompletion メソッドで Exception パラメータをチェックします。
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
if (ex != null) {
logger.error("Controller実行中に例外発生", ex);
}
}
Filter内で例外が発生した場合は、そのFilter内で処理するか、Springの ErrorController に委譲します。
詳しい例外処理の実装パターンは Spring BootのREST API例外ハンドリング を参照してください。
実行順序の制御
複数のFilterを使う場合、@Order アノテーションで順序を指定します。
@Component
@Order(1)
public class FirstFilter extends OncePerRequestFilter { /* ... */ }
@Component
@Order(2)
public class SecondFilter extends OncePerRequestFilter { /* ... */ }
より明示的に制御したい場合は FilterRegistrationBean を使います。
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new RequestLoggingFilter());
registrationBean.setOrder(1);
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
}
Interceptorの実行順序は addInterceptor の呼び出し順で決まります。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(firstInterceptor); // 先に実行
registry.addInterceptor(secondInterceptor); // 後に実行
}
使い分けの判断基準
実務で迷ったときの判断基準をまとめます。
Filterを選ぶべきケース
- Spring MVCに到達する前に処理が必要
- すべてのリクエスト(404含む)を処理したい
- サーブレット仕様の機能(リクエストラッパーなど)を使いたい
Interceptorを選ぶべきケース
- Controllerメソッドの情報が必要
- アノテーション情報を使った動的な処理をしたい
- Spring Beanへの依存が多い
- URL単位で細かく適用範囲を制御したい
迷ったらInterceptorを優先すると良いでしょう。Springのエコシステムとの親和性が高く、テストもしやすいです。
より高度な横断的関心事の実装が必要な場合は、AOPの利用も検討してください。Spring BootのAOPとは で詳しく解説しています。
まとめ
FilterとInterceptorは実行タイミングとアクセスできる情報が異なります。Filterはサーブレットレベル、InterceptorはSpring MVCレベルで動作します。
認証チェックやリクエストログなど、実装したい機能に応じて適切に使い分けることで、保守性の高いコードを書けます。基本的にはInterceptorを優先し、DispatcherServlet到達前の処理が必要な場合にFilterを使うと判断すれば迷いません。