My開発メモ

発生した例外をログとして記録する手順 (Java)

自作例外クラスを利用しながら、アプリで発生した例外をログとして残す方法を何とか書くことができたので、それをメモとしておく。

A. ログを開始する

1. ロガーを設定する

utilパッケージに LogConst.java を作成し、以下の内容とする。

package util;

import java.util.logging.Logger;

public class LogConst {
  // Loggerクラスのインスタンスを生成
  public static final Logger logger = Logger.getLogger(LogConst.class.getName());
}

Logger.getLogger(String name) の name については、APIに次の記述がある。

パラメータ: name – ロガーの名前。 これはドットで区切られた名前にすべきであり、 通常はjava.netやjavax.swingなど、サブシステムのパッケージ名または クラス名に基づいた名前にすべきである。( クラスLogger – getLogger )

2. リスナに以下を設定する。

package listener;

import java.io.File;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import util.LogConst;

@WebListener
public class LogInitListener implements ServletContextListener {
  
  private FileHandler fh;
  
  public void contextInitialized(ServletContextEvent sce)  { 
    try {
      String logPath = System.getProperty("catalina.home") 
          + File.separator
          + "logs"
          + File.separator
          + "sukkiriShop.log";
      fh = new FileHandler(
          logPath,
          1024 * 1024,  // 書き込み最大バイト数(16バイト単位)(デフォルトはゼロ)
          5,            // 循環する出力ファイル数(デフォルトは1)
          true);        // true -- ログを追記
      fh.setFormatter(new SimpleFormatter());
    
      // ロガーの初期化
      LogConst.logger.addHandler(fh);
      LogConst.logger.setLevel(Level.INFO);
      LogConst.logger.setUseParentHandlers(false);  // コンソール二重出力防止
      
      LogConst.logger.info("ログを出力しました");
      
      // デバッグ用
      printDebug();
      
    } catch (IOException e) {
      Logger.getGlobal().log(Level.SEVERE, "FileHander初期化失敗", e);
    }
  }

  public void contextDestroyed(ServletContextEvent sce)  { 
    if (fh != null) {
      fh.close();
    }
  }
  
  // デバッグ用: アプリケーションを起動したときにコンソールに表示される。
  private void printDebug() {
    System.out.println("config: " + System.getProperty("java.util.logging.config.file"));
    System.out.println("base: " + System.getProperty("catalina.base"));
    System.out.println("java_version: " + System.getProperty("java.version"));
    System.out.println("java.home: " + System.getProperty("java.home"));
    System.out.println("----------------------------");
    for (Handler h : LogConst.logger.getHandlers()) {
      System.out.println("handler: " + h.getClass().getName());
      System.out.println("formatter: " + h.getFormatter().getClass().getName());
    }
    String format = LogManager.getLogManager()
            .getProperty("java.util.logging.SimpleFormatter.format");
    System.out.println("format = " + format);
  }
}

logPath のところは、System.getProperty("catalina.home") としているので、Tomcatのフォルダに 出力される。 上記の設定では、僕の環境では、“~/bin/apache-tomcat-10.1.34/logs/sukkiriShop.log” となる。

クラスFileHandler – FileHandler に詳しい説明があるが、だいたいのところは、次のようである。

3. ログにスタックトレースを出力するようにする

例外が発生したときのスタックトレースをログに記録するようにする。

SimpleFormatter の場合は、上記コード中の printDebug()でその詳細がわかる。

この printDebug() の出力は、Ubuntuの場合とWindowsの場合で違う。

Ubuntu24.04の場合
config: null
base: /home/se-ichi/workspace/LaLa-servlet03-2026-07/.metadata/.plugins/org.eclipse.wst.server.core/tmp0
java_version: 21.0.11
java_home: /usr/lib/jvm/java-21-openjdk-amd64
----------------------------
handler: java.util.logging.FileHandler
formatter: java.util.logging.SimpleFormatter
format = null

config: null

base: /home … /.metadata/ … /tmp0

java_version: 21.0.11

java_home: /usr/lib/jvm/java-21-openjdi-amd64

handler: java.util.logging.FileHandler

formatter: java.util.logging.SimpeFormatter

format = null

Windows11の場合
config: null
base: C:\Users\user\Desktop\workspaces_home\LaLa-servlet03-2026-07-home\.metadata\.plugins\org.eclipse.wst.server.core\tmp0
java_version: 21.0.10
java_home: C:\pleiades\2026-03\java\21
----------------------------
handler: java.util.logging.FileHandler
formatter: java.util.logging.SimpleFormatter
format = %4$s: %5$s %6$s [%1$tc]%n

最後の行 format = %4$s: %5$s %6$s [%1$tc]%n を観ると、 %6 があるので、スタックトレースが出力される設定になっているのがわかる。

この場合の ログの形式は、 C:\preiades\2026-03\java\21\conf\logging.properties で 設定されているが、Windows11のデフォルトでは、

java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

となっていて、%6$s がない。 だから、ここに %6$s を書き加えてやると、スタックトレースが出力されるようになる。

4. ログ開始テスト

この時点で、アプリを再起動すると、Tomcatのlogsフォルダにログが出力されているはずである。

7月 19, 2026 7:13:41 午後 listener.LogInitListener contextInitialized
情報: ログを出力しました

B. SQL例外をログ出力する

1. 独自例外クラスを作成する。

アプリ全体の例外を集約するため、独自例外クラスを作成する。

業務エラー
package model;

/**
 * BusinessException
 * 業務エラーはこれを利用する
 * @author Seiichi Nukayama
 */
public class BusinessException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  public BusinessException(String message) {
    super(message);
  }

  public BusinessException(String message, Throwable cause) {
    super(message, cause);
  }
}
システムエラー
package model;

/**
 * SystemException
 * システムエラーはこれを利用する
 * @author Seiichi Nukayama
 */
public class SystemException extends RuntimeException {
  private static final long serialVersionUID = 1L;

  public SystemException(String message) {
    super(message);
  }

  public SystemException(String message, Throwable cause) {
    super(message, cause);
  }
}

両方とも、以下のコンストラクタを用意しているところがポイントである。

public BusinessException(String message, Throwable cause) {
  super(message, cause);
}
public SystemException(String message, Throwable cause) {
  super(message, cause);
}

第2引数で Throwableインスタンスを指定していることで、例外発生の原因の情報が作成され、それを例外インスタンスに引き継いでいる。

2. SQLExceptionをキャッチしている箇所を変更する

AccountsDAOファイルの各メソッドでは、次のようになっている。

try ( ...コネクション作成 ... ) {

  ... 処理

} catch (SQLException e) {
  e.printStackTrace();
  return null;    // あるいは return false;
}

これを次のように変更する。

try ( ...コネクション作成 ... ) {

  ... 処理

} catch (SQLException e) {
  throw new SystemException("データベースにエラーがありました", e);
}

3. フィルターで独自例外をキャッチする。

各サーブレットの実行時に例外が発生したら、キャッチできるように、以下のコードを書く。

package filter;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import model.BusinessException;
import model.SystemException;
import servlet.util.Const;
import util.LogConst;

public class ExceptionHandlingFilter extends HttpFilter {
       
  private static final long serialVersionUID = 1L;

  public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        chain.doFilter(request, response);
    
    } catch (BusinessException e) {
      String msg = makeMsg(request, "業務エラー", e);
      LogConst.logger.log(Level.SEVERE, msg, e);
      
      request.setAttribute("errorMessage", "業務エラーが発生しました");
      request.getRequestDispatcher("/WEB-INF/jsp_error/error.jsp").forward(request, response);
    
    } catch (SystemException e) {
      String msg = makeMsg(request, "システムエラー", e);
      LogConst.logger.log(Level.SEVERE, msg, e);
      
      if (response.isCommitted()) {
        System.out.printf("Exception after response committed: %s\n", e.getClass().getSimpleName());
        return;
      }
      request.setAttribute("errorMessage", "システムエラーが発生しました");
      request.getRequestDispatcher("/WEB-INF/jsp_error/systemError.jsp").forward(request, response);
    }
  }
  
  private String makeMsg(HttpServletRequest request, String errorMsg, Exception e) {
    HttpSession session = request.getSession(false);
    String user = null;
    if (session != null) {
      user = (String)session.getAttribute(Const.USER_ID);
    }
    String errorCause = null;
    Throwable cause = e.getCause();
    if (cause != null) {
      errorCause = cause.getClass().getSimpleName();
    }
    String uri = request.getRequestURI();
    String method = request.getMethod();
    String msg = String.format("[" + errorMsg + "] user=%s %s %s %s (%s)%n", user, uri, method, errorCause, e.getMessage());
    return msg;
  }
}

makeMsg() で、ログに残すためのメッセージを作成している。

  1. もしセッションに ユーザーID を保存していたら、そのユーザーID を記録する。
  2. ブラウザからリクエストされた URI を規則する。
  3. GET、POST のどちらかを記録する。
  4. 例外メッセージを記録する。

そのうえで、たとえば データベースでエラーが発生したら、詳しい情報はログに記録され、 アクセスしているユーザーには、「システムエラーが 発生しました」とだけ表示されるようにしている。

特に次の部分で、原因となった例外インスタンスを取得している。

Throwable cause = e.getCause();
if (cause != null) {
  errorCause = cause.getClass().getSimpleName();
}

この例外インスタンスのクラス名を msg の中に組み入れている。

error.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>スッキリ商店</title>
</head>
<body>
  <h2>業務エラー</h2>
  <c:if test="${not empty errorMsg}">
    <p><c:out value="${errorMsg}" /></p>
  </c:if>
  <p><a href="${pageContext.request.contextPath}/ReturnTopServlet">トップへ</a></p>
</body>
</html>
systemError.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>スッキリ商店</title>
</head>
<body>
  <h2>システムエラー</h2>
  <c:if test="${not empty errorMsg}">
    <p><c:out value="${errorMsg}" /></p>
  </c:if>
  <p><a href="${pageContext.request.contextPath}/ReturnTopServlet">トップへ</a></p>
</body>
</html>

C. アクセスログを記録する

次のようなフィルターを用意した。

package filter;

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;

import util.Const;
import util.LogConst;

public class AccessLogFilter extends HttpFilter implements Filter {

  private static final long serialVersionUID = 1L;

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

    long start = System.currentTimeMillis();
    boolean error = false;
    Exception caught = null;

    try {
      chain.doFilter(request, response);

    } catch (Exception e) {
      error = true;
      caught = e;
      throw e;
    } finally {
      long time = System.currentTimeMillis() - start;
      String uri = req.getRequestURI();
      String method = req.getMethod();
      String user = null;
      HttpSession session = req.getSession(false);
      if (session != null) {
          user = (String) session.getAttribute(Const.USER_ID);
      }
      if (error) {
        String msg = String.format("[ACCESS] 重大ERROR user=%s %s %s %dms (%s)%n",
            user, method, uri, time,
            caught.getClass().getSimpleName());
        LogConst.logger.severe(msg);
      } else {
        String msg = String.format("[ACCESS] user=%s %s %s %dms%n", user, method, uri, time);
        LogConst.logger.info(msg);
      }
    }
  }
}

これは、すべてのサーブレットで動作して、例外がなければ、「情報」として アクセスしたサーブレットクラスを出力する。例外が発生していれば、「重大」 として、ログを出力するものである。

また、例外が発生していれば、その例外インスタンスをスローもして、この フィルターの外側のフィルターに渡している。

D. フィルターの順番を決める。

フィルターの順番は、web.xmlファイルで指定する。
WEB-INFディレクトリ直下に配置する。

<?xml version="1.0" encoding="UTF-8"?>
<web-app
  xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
  version="6.0">

  <filter>
    <filter-name>exceptionFilter</filter-name>
    <filter-class>filter.ExceptionHandlingFilter</filter-class>
  </filter>
  
  <filter>
    <filter-name>accessLogFilter</filter-name>
    <filter-class>filter.AccessLogFilter</filter-class>
  </filter>
  
  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>filter.SetEncFilter</filter-class>
  </filter>
  
  <filter>
    <filter-name>checkLoginFilter</filter-name>
    <filter-class>filter.CheckLoginFilter</filter-class>
  </filter>
  
  <filter-mapping>
    <filter-name>exceptionFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>accessLogFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <filter-mapping>
    <filter-name>checkLoginFilter</filter-name>
    <url-pattern>/member/*</url-pattern>
  </filter-mapping>

  <welcome-file-list>
      <welcome-file>WelcomeServlet</welcome-file>
  </welcome-file-list>
</web-app>

フィルターの順番は、“filter-mapping” に記述した順に動作するということなので、

START -> ExceptionHandlingFilter
-> AccessLogFilter
-> SetEncFilter
-> CheckLoginFilter

という順になっている。

なお、web.xml に filter のURL等の指定を書くと、 各フィルターの @WebFilter() の指定は不要になる。

設定が食い違っているとまずいので、@WebFilter は削除したほうが いい。

E. 以上

以上で、ログが保存されているはずである。

sukkiriShop.log.0 (~/bin/apache-tomcat-10.1.34/logsディレクトリ)

7月 20, 2026 3:54:12 午後 listener.LogInitListener contextInitialized
情報: ログを出力しました
7月 20, 2026 3:54:30 午後 filter.AccessLogFilter doFilter
重大: [ACCESS] 重大ERROR user=null GET /sukkiriShop_nuk/member/MainServlet 0ms (BusinessException)

7月 20, 2026 3:54:30 午後 filter.ExceptionHandlingFilter doFilter
重大: [業務エラー] user=null /sukkiriShop_nuk/member/MainServlet GET (ログインしてください)

7月 20, 2026 3:54:33 午後 filter.AccessLogFilter doFilter
情報: [ACCESS] user=null GET /sukkiriShop_nuk/ReturnTopServlet 0ms

7月 20, 2026 3:54:33 午後 filter.AccessLogFilter doFilter
情報: [ACCESS] user=null GET /sukkiriShop_nuk/WelcomeServlet 4ms

カテゴリー: Eclipse, Java, memo, tomcat

タグ: java.util.Logger, ログ, 例外

カウント: 8