0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Java研修】Servlet/JSP入門⑨ フィルターとリスナー

0
Posted at

はじめに

Servlet/JSP入門の第9回は フィルターとリスナー です。

フィルターは、Servletの前後に共通処理を差し込む仕組みです。文字コードの設定、ログ出力、認証チェックなど、複数のServletに共通する処理を一箇所にまとめることができます。

リスナーは、Webアプリケーションやセッションのイベントを監視する仕組みです。アクセスカウンターやセッション管理に活用できます。

第9回で学ぶこと

  • フィルターとは何か、いつ使うか
  • Filterインターフェース(init, doFilter, destroy)
  • @WebFilterアノテーション
  • フィルターチェーンと実行順序
  • 実用的なフィルター(文字エンコーディング、認証、ログ)
  • リスナーとは何か
  • ServletContextListener, HttpSessionListener
  • 実用例:アクセスカウンター、セッション管理

1. フィルターとは?

フィルターの役割

フィルター(Filter) は、サーブレットやJSPの 前後 に処理を追加する仕組みです。

ブラウザ → [フィルター1] → [フィルター2] → Servlet → JSP
         ←  [フィルター2] ← [フィルター1] ←

フィルターの使いどころ

用途 説明
文字エンコーディング設定 すべてのリクエストにUTF-8を設定
認証・認可チェック ログインしていないユーザーをリダイレクト
ログ出力 リクエスト/レスポンスの情報をログに記録
レスポンスヘッダー設定 キャッシュ制御、セキュリティヘッダーを追加
アクセス制限 特定のIPアドレスからのアクセスを遮断

フィルターがない場合の問題

すべてのServletに同じコードを書く必要がある:

// ❌ 毎回書くのは面倒で忘れやすい
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    request.setCharacterEncoding("UTF-8");           // ← これを毎回書く
    response.setContentType("text/html; charset=UTF-8"); // ← これも毎回
    // ... 本来の処理
}

フィルターを使えば、この共通処理を一箇所にまとめられます。


2. Filterインターフェース

Filterの3つのメソッド

import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;

public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // フィルター初期化時に1回だけ呼ばれる
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // リクエストの前処理

        chain.doFilter(request, response);  // 次のフィルター or Servletへ

        // レスポンスの後処理
    }

    @Override
    public void destroy() {
        // フィルター破棄時に1回だけ呼ばれる
    }
}
メソッド タイミング 役割
init() アプリ起動時 初期化処理(設定の読み込み等)
doFilter() リクエストのたび 前処理 → 次へ渡す → 後処理
destroy() アプリ停止時 リソースの解放

doFilterの処理の流れ

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

    // ① 前処理(Servletが実行される前)
    System.out.println("前処理: リクエスト受信");

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

    // ③ 後処理(Servletが実行された後)
    System.out.println("後処理: レスポンス送信");
}

重要: chain.doFilter() を呼ばないと、リクエストがServletに到達しません。


3. @WebFilter アノテーション

基本的な書き方

import jakarta.servlet.annotation.WebFilter;

@WebFilter("/*")  // すべてのURLに適用
public class MyFilter implements Filter {
    // ...
}

URLパターンの指定方法

パターン 意味
/* すべてのリクエスト
/users/* /users/ 以下のリクエスト
*.jsp JSPファイルへのリクエスト
/api/* API系のリクエスト

アノテーションの属性

@WebFilter(
    filterName = "EncodingFilter",
    urlPatterns = {"/*"},
    initParams = {
        @WebInitParam(name = "encoding", value = "UTF-8")
    }
)
public class EncodingFilter implements Filter {
    // ...
}

4. フィルターチェーンと実行順序

複数のフィルター

複数のフィルターを設定すると、チェーン(連鎖) として順番に実行されます。

リクエスト
  → EncodingFilter.doFilter()(前処理)
    → LoggingFilter.doFilter()(前処理)
      → AuthFilter.doFilter()(前処理)
        → Servlet.doGet()
      ← AuthFilter.doFilter()(後処理)
    ← LoggingFilter.doFilter()(後処理)
  ← EncodingFilter.doFilter()(後処理)
レスポンス

実行順序の制御

@WebFilter アノテーションだけでは実行順序を制御できません。順序を制御するには web.xml を使います。

<!-- web.xml で順序を制御 -->
<filter>
    <filter-name>EncodingFilter</filter-name>
    <filter-class>filter.EncodingFilter</filter-class>
</filter>
<filter>
    <filter-name>LoggingFilter</filter-name>
    <filter-class>filter.LoggingFilter</filter-class>
</filter>
<filter>
    <filter-name>AuthFilter</filter-name>
    <filter-class>filter.AuthFilter</filter-class>
</filter>

<!-- filter-mapping の記述順で実行順序が決まる -->
<filter-mapping>
    <filter-name>EncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>LoggingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>AuthFilter</filter-name>
    <url-pattern>/admin/*</url-pattern>
</filter-mapping>

<filter-mapping> の記述順序が上から順に実行されます。


5. 実用的なフィルター

5.1 文字エンコーディングフィルター

最もよく使うフィルターです。 すべてのリクエスト・レスポンスにUTF-8を設定します。

package filter;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;

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

    private String encoding = "UTF-8";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初期化パラメータから文字コードを取得(設定されていなければデフォルトUTF-8)
        String configEncoding = filterConfig.getInitParameter("encoding");
        if (configEncoding != null) {
            this.encoding = configEncoding;
        }
        System.out.println("[EncodingFilter] 初期化: encoding=" + encoding);
    }

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

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

        // 次のフィルター or Servletへ
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        System.out.println("[EncodingFilter] 破棄");
    }
}

これにより、個々のServletで request.setCharacterEncoding("UTF-8") を書く必要がなくなります。

5.2 ログ出力フィルター

リクエストの情報(URL、メソッド、処理時間)をログに出力します。

package filter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

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

    private static final DateTimeFormatter FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("[LoggingFilter] 初期化");
    }

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

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        String method = httpRequest.getMethod();
        String uri = httpRequest.getRequestURI();
        String queryString = httpRequest.getQueryString();
        String clientIP = httpRequest.getRemoteAddr();
        String timestamp = LocalDateTime.now().format(FORMATTER);

        // 前処理:リクエスト情報をログ出力
        System.out.println("[アクセスログ] " + timestamp
                + " | " + method
                + " " + uri
                + (queryString != null ? "?" + queryString : "")
                + " | IP: " + clientIP);

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

        // 次のフィルター or Servletへ
        chain.doFilter(request, response);

        // 後処理:処理時間をログ出力
        long endTime = System.currentTimeMillis();
        System.out.println("[処理時間] " + uri + " → " + (endTime - startTime) + "ms");
    }

    @Override
    public void destroy() {
        System.out.println("[LoggingFilter] 破棄");
    }
}

コンソール出力例:

[アクセスログ] 2025-04-15 14:30:00 | GET /WebStudy/users | IP: 127.0.0.1
[処理時間] /WebStudy/users → 45ms

5.3 認証フィルター

ログインしていないユーザーを、ログインページにリダイレクトします。

package filter;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

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

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("[AuthFilter] 初期化");
    }

    @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);
        boolean isLoggedIn = (session != null && session.getAttribute("loginUser") != null);

        if (isLoggedIn) {
            // ログイン済み → そのまま通す
            chain.doFilter(request, response);
        } else {
            // 未ログイン → ログインページへリダイレクト
            System.out.println("[AuthFilter] 未認証アクセスをブロック: " + httpRequest.getRequestURI());
            httpResponse.sendRedirect(httpRequest.getContextPath() + "/login");
        }
    }

    @Override
    public void destroy() {
        System.out.println("[AuthFilter] 破棄");
    }
}

ポイント: /admin/* にマッチするURLへのアクセスだけがこのフィルターを通ります。ログインページ(/login)にはフィルターを適用しないことが重要です(無限リダイレクトを防ぐため)。

フィルターの実装パターン比較

フィルター URLパターン 処理内容
EncodingFilter /* 文字コード設定
LoggingFilter /* アクセスログ出力
AuthFilter /admin/* ログインチェック

6. リスナーとは?

リスナーの役割

リスナー(Listener) は、Webアプリケーション内で発生する イベント を監視し、自動的に処理を実行する仕組みです。

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

インターフェース 監視対象 イベント
ServletContextListener アプリケーション 起動・停止
ServletContextAttributeListener アプリケーション属性 属性の追加・削除・変更
HttpSessionListener セッション 作成・破棄
HttpSessionAttributeListener セッション属性 属性の追加・削除・変更
ServletRequestListener リクエスト リクエスト開始・終了

7. ServletContextListener

アプリケーションの起動・停止を監視

package listener;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class AppInitListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // アプリケーション起動時に呼ばれる
        ServletContext context = sce.getServletContext();

        String startTime = LocalDateTime.now().format(
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

        // アプリケーション全体で共有するデータを初期化
        context.setAttribute("appStartTime", startTime);
        context.setAttribute("accessCount", 0);

        System.out.println("=== アプリケーション起動 [" + startTime + "] ===");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // アプリケーション停止時に呼ばれる
        System.out.println("=== アプリケーション停止 ===");
    }
}

ポイント: @WebListener アノテーションを付けるだけでリスナーとして登録されます。contextInitialized はアプリ起動時に1回だけ呼ばれるため、初期化処理(DB接続プール作成、設定ファイル読み込みなど)に適しています。


8. HttpSessionListener

セッションの作成・破棄を監視

package listener;

import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;

@WebListener
public class SessionCountListener implements HttpSessionListener {

    private static int activeSessions = 0;

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        activeSessions++;
        System.out.println("[Session] 作成 ID: " + se.getSession().getId()
                + " | アクティブセッション数: " + activeSessions);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        activeSessions--;
        System.out.println("[Session] 破棄 ID: " + se.getSession().getId()
                + " | アクティブセッション数: " + activeSessions);
    }

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

9. 実用例:アクセスカウンター

フィルターとリスナーを組み合わせて、アプリケーション全体のアクセスカウンターを実装します。

AppInitListener.java(リスナー)

package listener;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;

@WebListener
public class AppInitListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        context.setAttribute("totalAccess", 0);
        System.out.println("[AppInit] アクセスカウンターを初期化しました");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        Integer count = (Integer) sce.getServletContext().getAttribute("totalAccess");
        System.out.println("[AppInit] アプリ停止 - 合計アクセス数: " + count);
    }
}

AccessCountFilter.java(フィルター)

package filter;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;

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

    private ServletContext context;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        context = filterConfig.getServletContext();
    }

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

        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // 静的リソース(CSS, JS, 画像)はカウントしない
        String uri = httpRequest.getRequestURI();
        if (!uri.endsWith(".css") && !uri.endsWith(".js") && !uri.endsWith(".png")
                && !uri.endsWith(".jpg") && !uri.endsWith(".ico")) {

            synchronized (context) {
                Integer count = (Integer) context.getAttribute("totalAccess");
                context.setAttribute("totalAccess", count + 1);
            }
        }

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
}

アクセスカウンターを表示するServlet

package servlet;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import listener.SessionCountListener;

@WebServlet("/status")
public class StatusServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        Integer totalAccess = (Integer) getServletContext().getAttribute("totalAccess");
        String startTime = (String) getServletContext().getAttribute("appStartTime");
        int activeSessions = SessionCountListener.getActiveSessions();

        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>サーバーステータス</h1>");
        out.println("<table border='1'>");
        out.println("<tr><th>項目</th><th>値</th></tr>");
        out.println("<tr><td>アプリ起動時刻</td><td>" + startTime + "</td></tr>");
        out.println("<tr><td>合計アクセス数</td><td>" + totalAccess + "</td></tr>");
        out.println("<tr><td>アクティブセッション数</td><td>" + activeSessions + "</td></tr>");
        out.println("</table>");
        out.println("</body></html>");
    }
}

10. 実用例:セッション管理ダッシュボード

ログイン中のユーザーを一覧表示するリスナーを実装します。

OnlineUsersListener.java

package listener;

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionAttributeListener;
import jakarta.servlet.http.HttpSessionBindingEvent;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;

@WebListener
public class OnlineUsersListener implements HttpSessionListener, HttpSessionAttributeListener {

    // セッションID → ユーザー名のマップ
    private static final Map<String, String> onlineUsers = new ConcurrentHashMap<>();

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        // セッション破棄時にオンラインユーザーから削除
        String sessionId = se.getSession().getId();
        String removedUser = onlineUsers.remove(sessionId);
        if (removedUser != null) {
            System.out.println("[Online] ログアウト: " + removedUser);
        }
    }

    @Override
    public void attributeAdded(HttpSessionBindingEvent event) {
        // "loginUser" 属性が追加されたらオンラインユーザーに登録
        if ("loginUser".equals(event.getName())) {
            String sessionId = event.getSession().getId();
            String userName = (String) event.getValue();
            onlineUsers.put(sessionId, userName);
            System.out.println("[Online] ログイン: " + userName);
        }
    }

    @Override
    public void attributeRemoved(HttpSessionBindingEvent event) {
        if ("loginUser".equals(event.getName())) {
            String sessionId = event.getSession().getId();
            onlineUsers.remove(sessionId);
        }
    }

    @Override
    public void attributeReplaced(HttpSessionBindingEvent event) {}

    @Override
    public void sessionCreated(HttpSessionEvent se) {}

    // オンラインユーザー一覧を取得
    public static Map<String, String> getOnlineUsers() {
        return onlineUsers;
    }
}

練習問題

問題1:レスポンスヘッダー追加フィルター ⭐

すべてのレスポンスに以下のセキュリティヘッダーを追加するフィルターを作成してください。

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Cache-Control: no-cache, no-store
模範解答
package filter;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse;

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

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("[SecurityHeaderFilter] 初期化");
    }

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

        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // セキュリティヘッダーを追加
        httpResponse.setHeader("X-Content-Type-Options", "nosniff");
        httpResponse.setHeader("X-Frame-Options", "DENY");
        httpResponse.setHeader("Cache-Control", "no-cache, no-store");

        // 次のフィルター or Servletへ
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        System.out.println("[SecurityHeaderFilter] 破棄");
    }
}

ポイント: レスポンスヘッダーの追加は chain.doFilter() の前に行います。セキュリティヘッダーはWebアプリケーションの基本的なセキュリティ対策です。X-Content-Type-Options: nosniff はMIMEタイプのスニッフィングを防ぎ、X-Frame-Options: DENY はクリックジャッキング攻撃を防ぎます。

問題2:メンテナンスモードフィルター ⭐⭐

ServletContextmaintenanceMode 属性が true の場合、すべてのリクエストに対して「メンテナンス中です」と表示するフィルターを作成してください。

  • /admin/* はメンテナンスモード中でもアクセス可能にする
  • メンテナンスモードの切り替えはServletで行う
模範解答

MaintenanceFilter.java

package filter;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

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

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

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

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 管理者ページはメンテナンスモード中でもアクセス可能
        String uri = httpRequest.getRequestURI();
        String contextPath = httpRequest.getContextPath();
        if (uri.startsWith(contextPath + "/admin")) {
            chain.doFilter(request, response);
            return;
        }

        // メンテナンスモード判定
        Boolean maintenanceMode = (Boolean) httpRequest.getServletContext()
                .getAttribute("maintenanceMode");

        if (maintenanceMode != null && maintenanceMode) {
            // メンテナンス中ページを表示
            httpResponse.setContentType("text/html; charset=UTF-8");
            httpResponse.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
            PrintWriter out = httpResponse.getWriter();
            out.println("<!DOCTYPE html>");
            out.println("<html><head><title>メンテナンス中</title></head>");
            out.println("<body style='text-align:center; padding-top:100px; font-family:sans-serif;'>");
            out.println("<h1>メンテナンス中</h1>");
            out.println("<p>現在、システムメンテナンスを実施しています。</p>");
            out.println("<p>しばらくしてから再度アクセスしてください。</p>");
            out.println("</body></html>");
            // chain.doFilterを呼ばない → Servletに到達しない
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {}
}

MaintenanceControlServlet.java(メンテナンスモードの切り替え用)

package servlet;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet("/admin/maintenance")
public class MaintenanceControlServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        Boolean current = (Boolean) getServletContext().getAttribute("maintenanceMode");
        if (current == null) current = false;

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>メンテナンスモード管理</h1>");
        out.println("<p>現在の状態: <strong>" + (current ? "メンテナンス中" : "通常稼働") + "</strong></p>");
        out.println("<form method='post'>");
        out.println("<button name='mode' value='on'>メンテナンスON</button> ");
        out.println("<button name='mode' value='off'>メンテナンスOFF</button>");
        out.println("</form>");
        out.println("</body></html>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String mode = request.getParameter("mode");
        boolean maintenance = "on".equals(mode);
        getServletContext().setAttribute("maintenanceMode", maintenance);

        response.sendRedirect(request.getRequestURI());
    }
}

ポイント: chain.doFilter() を呼ばないことで、リクエストをブロックできます。メンテナンスモードのようにアプリ全体に影響する制御は、フィルターで一元管理するのが適切です。

問題3:リクエスト処理時間の統計リスナー ⭐⭐

ServletRequestListener を使って、各リクエストの処理時間を計測し、統計情報(合計リクエスト数、平均処理時間、最長処理時間)を記録するリスナーを作成してください。

  • リクエスト開始時に開始時刻を属性として記録
  • リクエスト終了時に処理時間を計算して統計を更新
  • /status/performance でパフォーマンス統計を確認できるServletも作成
模範解答

PerformanceListener.java

package listener;

import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletRequestEvent;
import jakarta.servlet.ServletRequestListener;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpServletRequest;

@WebListener
public class PerformanceListener implements ServletRequestListener {

    private static long totalRequests = 0;
    private static long totalTime = 0;
    private static long maxTime = 0;
    private static String maxTimeUrl = "";

    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        // リクエスト開始時刻を記録
        sre.getServletRequest().setAttribute("startTime", System.currentTimeMillis());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        ServletRequest request = sre.getServletRequest();
        Long startTime = (Long) request.getAttribute("startTime");

        if (startTime != null) {
            long processingTime = System.currentTimeMillis() - startTime;

            synchronized (PerformanceListener.class) {
                totalRequests++;
                totalTime += processingTime;

                if (processingTime > maxTime) {
                    maxTime = processingTime;
                    if (request instanceof HttpServletRequest) {
                        maxTimeUrl = ((HttpServletRequest) request).getRequestURI();
                    }
                }
            }
        }
    }

    public static long getTotalRequests() { return totalRequests; }
    public static double getAverageTime() {
        return totalRequests > 0 ? (double) totalTime / totalRequests : 0;
    }
    public static long getMaxTime() { return maxTime; }
    public static String getMaxTimeUrl() { return maxTimeUrl; }
}

PerformanceServlet.java

package servlet;

import java.io.IOException;
import java.io.PrintWriter;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import listener.PerformanceListener;

@WebServlet("/status/performance")
public class PerformanceServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.println("<!DOCTYPE html>");
        out.println("<html><body>");
        out.println("<h1>パフォーマンス統計</h1>");
        out.println("<table border='1'>");
        out.println("<tr><th>項目</th><th>値</th></tr>");
        out.println("<tr><td>合計リクエスト数</td><td>" + PerformanceListener.getTotalRequests() + "</td></tr>");
        out.println("<tr><td>平均処理時間</td><td>" + String.format("%.2f", PerformanceListener.getAverageTime()) + " ms</td></tr>");
        out.println("<tr><td>最長処理時間</td><td>" + PerformanceListener.getMaxTime() + " ms</td></tr>");
        out.println("<tr><td>最長処理URL</td><td>" + PerformanceListener.getMaxTimeUrl() + "</td></tr>");
        out.println("</table>");
        out.println("<p><a href='" + request.getRequestURI() + "'>更新</a></p>");
        out.println("</body></html>");
    }
}

ポイント: ServletRequestListenerrequestInitialized はリクエスト開始時、requestDestroyed はリクエスト終了時に呼ばれます。リクエスト属性に開始時刻を保存しておき、終了時に差分を取ることで処理時間を計測できます。synchronized で排他制御しているのは、複数リクエストが同時に統計を更新する可能性があるためです。


まとめ

学んだこと キーワード
フィルターの基本 Filter インターフェース、doFilterFilterChain
フィルターの設定 @WebFilter、URLパターン、web.xml での順序制御
実用的なフィルター エンコーディング、ログ出力、認証チェック
リスナーの基本 @WebListener、イベント監視
ServletContextListener アプリ起動・停止時の処理
HttpSessionListener セッション作成・破棄の監視
組み合わせ アクセスカウンター、パフォーマンス計測

次回はいよいよ最終回! 総合演習:掲示板アプリを作ろう です!


シリーズ一覧:Servlet/JSP入門

  1. 環境構築とはじめてのServlet
  2. HTTPリクエストとレスポンス
  3. JSPの基礎
  4. フォーム処理(GET/POST)
  5. セッション管理とCookie
  6. MVCパターン(Servlet + JSP)
  7. JDBC連携(データベース操作)
  8. EL式とJSTL
  9. 👉 フィルターとリスナー(本記事)
  10. 総合演習:掲示板アプリを作ろう

著者: @kotaro_ai_lab
AI駆動開発やテック情報を毎日発信しています。フォローお気軽にどうぞ!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?