はじめに
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: nosniffX-Frame-Options: DENYCache-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:メンテナンスモードフィルター ⭐⭐
ServletContext の maintenanceMode 属性が 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>");
}
}
ポイント: ServletRequestListener の requestInitialized はリクエスト開始時、requestDestroyed はリクエスト終了時に呼ばれます。リクエスト属性に開始時刻を保存しておき、終了時に差分を取ることで処理時間を計測できます。synchronized で排他制御しているのは、複数リクエストが同時に統計を更新する可能性があるためです。
まとめ
| 学んだこと | キーワード |
|---|---|
| フィルターの基本 |
Filter インターフェース、doFilter、FilterChain
|
| フィルターの設定 |
@WebFilter、URLパターン、web.xml での順序制御 |
| 実用的なフィルター | エンコーディング、ログ出力、認証チェック |
| リスナーの基本 |
@WebListener、イベント監視 |
| ServletContextListener | アプリ起動・停止時の処理 |
| HttpSessionListener | セッション作成・破棄の監視 |
| 組み合わせ | アクセスカウンター、パフォーマンス計測 |
次回はいよいよ最終回! 総合演習:掲示板アプリを作ろう です!
シリーズ一覧:Servlet/JSP入門
- 環境構築とはじめてのServlet
- HTTPリクエストとレスポンス
- JSPの基礎
- フォーム処理(GET/POST)
- セッション管理とCookie
- MVCパターン(Servlet + JSP)
- JDBC連携(データベース操作)
- EL式とJSTL
- 👉 フィルターとリスナー(本記事)
- 総合演習:掲示板アプリを作ろう
著者: @kotaro_ai_lab
AI駆動開発やテック情報を毎日発信しています。フォローお気軽にどうぞ!