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は HttpServletRequestHttpServletResponse のみにアクセスできます。

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/**");
    }
}

addPathPatternsexcludePathPatterns でInterceptorを適用するURL範囲を制御できます。

リクエスト/レスポンスログの実装

全リクエストのログを取りたい場合はFilterを使います。DispatcherServletに到達しなかったリクエスト(404など)も記録できます。

Controllerメソッド単位でログを取りたい場合はInterceptorが適しています。どのControllerメソッドが呼ばれたかの情報を記録できます。

リクエストボディやレスポンスボディをログに出力したい場合は、ContentCachingRequestWrapperContentCachingResponseWrapper を使います。

@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を使うと判断すれば迷いません。