フィルタとリスナ – Servletの高度な機能

2025-08-04

はじめに

Webアプリケーション開発において、フィルタ(Filter)リスナ(Listener)は非常に強力な機能です。これらを適切に使用することで、アプリケーション全体にわたる共通処理を効率的に実装したり、アプリケーションのライフサイクルイベントを捕捉したりできます。本記事では、Servlet APIの一部として提供されるこれらの機能について、実践的な例を交えながら詳しく解説します。

フィルタ(Filter)の基本

フィルタとは?

フィルタは、クライアントからのリクエストがServletに到達する前、およびServletからのレスポンスがクライアントに返される前に処理を挟み込むことができるコンポーネントです。以下のような特徴があります:

  • 複数のフィルタを連鎖(チェーン)させることが可能
  • 特定のURLパターンやServletに対して適用可能
  • リクエストとレスポンスをラップして加工可能
  • 認証、ロギング、エンコーディング変換などに利用される

フィルタの主な用途

  1. 認証/認可処理:ログインしていないユーザーのアクセス制限
  2. ロギング:リクエスト情報の記録
  3. エンコーディング設定:リクエスト/レスポンスの文字エンコーディング統一
  4. データ圧縮:レスポンスデータの圧縮
  5. XSS対策:リクエストパラメータのサニタイズ
  6. レスポンスの加工:コンテンツの置換や追加

フィルタの実装方法

基本的なフィルタの実装例

@WebFilter("/*") // すべてのリクエストに適用
public class BasicFilter implements Filter {

    private FilterConfig filterConfig;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
        System.out.println("BasicFilterが初期化されました");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {
        System.out.println("BasicFilter: 前処理");

        // リクエストの前処理
        long startTime = System.currentTimeMillis();

        // 次のフィルタまたはServletへ処理を渡す
        chain.doFilter(request, response);

        // レスポンスの後処理
        long endTime = System.currentTimeMillis();
        System.out.println("リクエスト処理時間: " + (endTime - startTime) + "ms");

        System.out.println("BasicFilter: 後処理");
    }

    @Override
    public void destroy() {
        System.out.println("BasicFilterが破棄されました");
    }
}

フィルタのライフサイクル

  1. 初期化(init):アプリケーション起動時に1回だけ呼ばれる
  2. フィルタ処理(doFilter):リクエストごとに呼ばれる
  3. 破棄(destroy):アプリケーション停止時に呼ばれる

フィルタチェーンの概念

複数のフィルタが登録されている場合、以下の順序で処理が行われます:

クライアント → Filter1 → Filter2 → ... → FilterN → Servlet → FilterN → ... → Filter2 → Filter1 → クライアント

実践的なフィルタの例

認証フィルタの実装

@WebFilter("/secure/*")
public class AuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpSession session = httpRequest.getSession(false);

        // セッションにユーザー情報がない場合はログインページへリダイレクト
        if (session == null || session.getAttribute("user") == null) {
            httpResponse.sendRedirect(httpRequest.getContextPath() + "/login");
            return;
        }

        // 認証済みの場合は次の処理へ
        chain.doFilter(request, response);
    }

    // initとdestroyは省略(必要に応じて実装)
}

ロギングフィルタの実装

@WebFilter("/*")
public class LoggingFilter implements Filter {

    private static final Logger logger = Logger.getLogger(LoggingFilter.class.getName());

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // リクエスト情報をログに記録
        String requestInfo = String.format(
            "RemoteAddr=%s, Method=%s, URL=%s, Parameters=%s",
            httpRequest.getRemoteAddr(),
            httpRequest.getMethod(),
            httpRequest.getRequestURL(),
            httpRequest.getParameterMap()
        );

        logger.info("Request: " + requestInfo);

        // 処理時間の計測
        long startTime = System.currentTimeMillis();

        try {
            chain.doFilter(request, response);
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            logger.info("Request processed in " + duration + "ms");
        }
    }
}

エンコーディングフィルタの実装

@WebFilter("/*")
public class EncodingFilter implements Filter {

    private String encoding;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.encoding = filterConfig.getInitParameter("encoding");
        if (this.encoding == null) {
            this.encoding = "UTF-8";
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {

        // リクエストとレスポンスのエンコーディングを設定
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);

        chain.doFilter(request, response);
    }
}

リスナ(Listener)の基本

リスナとは?

リスナは、Webアプリケーションのライフサイクルイベントや、セッション・リクエストの状態変化を監視するためのコンポーネントです。以下のような特徴があります:

  • アプリケーションの起動/停止を検知できる
  • セッションの作成/破棄を検知できる
  • リクエストの開始/終了を検知できる
  • 属性の変更を検知できる

リスナの主な用途

  1. アプリケーション初期化処理:データベース接続プールの初期化など
  2. リソース管理:アプリケーション終了時のリソース解放
  3. 統計情報の収集:アクティブなセッション数の監視
  4. セッション管理:タイムアウトしたセッションの後処理
  5. キャッシュ管理:アプリケーション起動時にキャッシュをプリロード

リスナの種類と実装方法

主要なリスナインターフェース

  1. ServletContextListener:アプリケーションのライフサイクルイベント
  2. ServletContextAttributeListener:アプリケーションスコープの属性変更
  3. HttpSessionListener:セッションのライフサイクルイベント
  4. HttpSessionAttributeListener:セッションスコープの属性変更
  5. ServletRequestListener:リクエストのライフサイクルイベント
  6. ServletRequestAttributeListener:リクエストスコープの属性変更

ServletContextListenerの実装例

@WebListener
public class AppContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("アプリケーションが起動しました");

        // データベース接続プールの初期化など
        ServletContext ctx = sce.getServletContext();
        String jdbcUrl = ctx.getInitParameter("jdbcUrl");

        try {
            ConnectionPool pool = new ConnectionPool(jdbcUrl);
            ctx.setAttribute("connectionPool", pool);
            System.out.println("データベース接続プールが初期化されました");
        } catch (SQLException e) {
            System.err.println("データベース接続プールの初期化に失敗しました");
            e.printStackTrace();
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("アプリケーションが停止します");

        // リソースの解放
        ServletContext ctx = sce.getServletContext();
        ConnectionPool pool = (ConnectionPool) ctx.getAttribute("connectionPool");

        if (pool != null) {
            pool.closeAllConnections();
            System.out.println("データベース接続プールが解放されました");
        }
    }
}

HttpSessionListenerの実装例

@WebListener
public class SessionCounterListener implements HttpSessionListener {

    private static final AtomicInteger activeSessions = new AtomicInteger();

    public static int getActiveSessions() {
        return activeSessions.get();
    }

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        activeSessions.incrementAndGet();
        System.out.println("セッションが作成されました。現在のアクティブセッション数: " + activeSessions.get());
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        activeSessions.decrementAndGet();
        System.out.println("セッションが破棄されました。現在のアクティブセッション数: " + activeSessions.get());
    }
}

ServletRequestListenerの実装例

@WebListener
public class RequestStatsListener implements ServletRequestListener {

    private static final AtomicLong totalRequests = new AtomicLong();
    private static final AtomicLong totalProcessingTime = new AtomicLong();

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        sre.getServletRequest().setAttribute("startTime", System.currentTimeMillis());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        Long startTime = (Long) sre.getServletRequest().getAttribute("startTime");
        if (startTime != null) {
            long duration = System.currentTimeMillis() - startTime;
            totalRequests.incrementAndGet();
            totalProcessingTime.addAndGet(duration);

            System.out.printf("リクエスト処理完了: 処理時間=%dms, 平均処理時間=%.2fms%n",
                duration,
                (double) totalProcessingTime.get() / totalRequests.get());
        }
    }

    public static long getTotalRequests() {
        return totalRequests.get();
    }

    public static double getAverageProcessingTime() {
        return totalRequests.get() > 0 
            ? (double) totalProcessingTime.get() / totalRequests.get() 
            : 0;
    }
}

フィルタとリスナの応用例

パフォーマンスモニタリングシステム

@WebListener
public class PerformanceMonitor implements ServletContextListener, 
        ServletRequestListener, HttpSessionListener {

    private static final Logger logger = Logger.getLogger(PerformanceMonitor.class.getName());

    private ScheduledExecutorService scheduler;

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        scheduler = Executors.newScheduledThreadPool(1);

        // 5分ごとにパフォーマンス統計をログに出力
        scheduler.scheduleAtFixedRate(() -> {
            ServletContext ctx = sce.getServletContext();

            long totalMemory = Runtime.getRuntime().totalMemory();
            long freeMemory = Runtime.getRuntime().freeMemory();
            long usedMemory = totalMemory - freeMemory;

            int activeSessions = SessionCounterListener.getActiveSessions();
            long totalRequests = RequestStatsListener.getTotalRequests();
            double avgProcessingTime = RequestStatsListener.getAverageProcessingTime();

            String stats = String.format(
                "Performance Stats: Memory Used=%dMB, Active Sessions=%d, " +
                "Total Requests=%d, Avg Processing Time=%.2fms",
                usedMemory / (1024 * 1024),
                activeSessions,
                totalRequests,
                avgProcessingTime
            );

            logger.info(stats);
            ctx.log(stats);
        }, 0, 5, TimeUnit.MINUTES);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        if (scheduler != null) {
            scheduler.shutdownNow();
        }
    }

    // その他のリスナメソッドは省略...
}

マルチテナンシー対応フィルタ

@WebFilter("/*")
public class TenantIdentificationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
            FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // テナント識別子を取得(サブドメイン、ヘッダー、パスなどから)
        String tenantId = identifyTenant(httpRequest);

        // テナントごとの設定を適用
        applyTenantSettings(tenantId, httpRequest);

        // リクエスト属性にテナントIDを設定
        httpRequest.setAttribute("tenantId", tenantId);

        chain.doFilter(request, response);
    }

    private String identifyTenant(HttpServletRequest request) {
        // サブドメインからテナントを識別(例: tenant1.example.com)
        String serverName = request.getServerName();
        if (serverName.contains(".")) {
            return serverName.split("\\.")[0];
        }

        // ヘッダーからテナントを識別
        String tenantHeader = request.getHeader("X-Tenant-ID");
        if (tenantHeader != null && !tenantHeader.isEmpty()) {
            return tenantHeader;
        }

        // デフォルトテナント
        return "default";
    }

    private void applyTenantSettings(String tenantId, HttpServletRequest request) {
        // テナントごとのデータソース設定
        // テナントごとのテーマ設定
        // テナントごとのロケール設定など
    }
}

ベストプラクティスと注意点

フィルタのベストプラクティス

  1. フィルタの順序に注意:依存関係があるフィルタは順序を考慮する(web.xmlでの順序が重要)
  2. スレッドセーフな実装:フィルタは複数スレッドから呼ばれる可能性がある
  3. リソースリーク防止:フィルタで開いたリソースは確実に閉じる
  4. パフォーマンス考慮:フィルタの処理は可能な限り軽量に
  5. 例外処理:適切なエラーページへリダイレクトするなどの例外処理を実装

リスナのベストプラクティス

  1. 初期化処理の失敗処理:contextInitializedで例外が発生するとアプリケーションが起動しない
  2. 長時間実行する処理を避ける:リスナのメソッドはブロックしないように
  3. スレッドセーフな実装:共有リソースへのアクセスは同期化する
  4. メモリリーク防止:リスナで保持した参照は適切に解放
  5. リスナの登録順序:リスナの初期化順序は保証されないので依存関係を作らない

よくある問題と解決策

問題1:フィルタが適用されない

原因

  • URLパターンの不一致
  • web.xmlの設定ミス(アノテーションと競合)
  • フィルタチェーンの途中で処理が中断されている

解決策

  • デバッグログを追加してフィルタが呼ばれているか確認
  • URLパターンを再確認
  • web.xmlとアノテーションの競合を解消

問題2:リスナがイベントを捕捉しない

原因

  • リスナが正しく登録されていない
  • スコープが異なる(アプリケーションリスナなのにセッションイベントを期待など)
  • 例外が発生してリスナが無効化されている

解決策

  • @WebListenerアノテーションまたはweb.xmlの設定を確認
  • 適切なリスナインターフェースを実装しているか確認
  • 例外スタックトレースを調査

問題3:フィルタでレスポンスを加工するとパフォーマンスが低下

原因

  • 大きなレスポンスコンテンツをメモリに保持している
  • 複雑な正規表現処理を行っている
  • 同期処理がボトルネックになっている

解決策

  • ストリーム処理を採用する
  • キャッシュを導入する
  • 非同期処理を検討する

まとめ

フィルタとリスナは、Servlet仕様が提供する強力な機能であり、Webアプリケーション開発において以下のような利点があります:

  1. 横断的関心事の分離:認証、ロギング、エンコーディングなどの共通処理を個別のコンポーネントとして分離できる
  2. アプリケーションライフサイクルの管理:起動時/停止時のリソース管理をシステマティックに行える
  3. 拡張性の向上:既存のServletに影響を与えずに新機能を追加できる
  4. 保守性の向上:共通処理が一箇所に集約されるためメンテナンスが容易

これらの機能を適切に活用することで、より構造化され、保守性の高いWebアプリケーションを構築できます。実際の開発では、アプリケーションの要件に応じて、ここで学んだ基本パターンを適応させて使用してください。

さらに高度な使用方法として、フィルタとリスナを組み合わせた設計パターンや、非同期処理との連携、マイクロサービスアーキテクチャでの活用など、さまざまな応用が可能です。基礎をしっかり理解した上で、これらの応用技術にも挑戦していくと良いでしょう。