Javaサーブレットのエラーハンドリング完全ガイド!例外処理とエラーページのカスタマイズ
新人
「先輩、JavaでWebアプリを作っているのですが、プログラムでミスをすると真っ白な画面や、英語だらけの怖いエラー画面が出てしまいます。これってどうにかならないんでしょうか?」
先輩
「それは『例外』が発生したときのデフォルトの表示ですね。ユーザーがそれを見てしまうと、サイトが壊れたと思って離脱してしまいます。Webアプリでは『エラーハンドリング』という仕組みを使って、適切な案内を表示させるのが鉄則ですよ。」
新人
「エラーハンドリング……難しそうですが、どうやって設定するんですか?」
先輩
「実はweb.xmlという設定ファイルや、Javaのアノテーションを使うだけで、意外と簡単にカスタマイズできるんです。基本から一緒に学んでいきましょう!」
1. サーブレットにおけるエラーハンドリングの基本
JavaのWeb開発(サーブレット/JSP)におけるエラーハンドリングとは、プログラムの実行中に予期せぬ問題が発生した際に、それを検知して適切な処理を行う仕組みのことを指します。プログラミングの世界では、想定外の事態を「例外(Exception)」と呼びます。
計算式で「0」で割り算をしてしまったり、存在しないファイルを開こうとしたりしたときに発生する「エラー」の一種です。Javaでは、これが発生するとプログラムが途中で止まってしまいます。
Webアプリケーションにおいて、エラーハンドリングが重要な理由は主に3つあります。1つ目は、ユーザー体験(UX)の向上です。システム内部の難しいメッセージが表示される代わりに、「ただいまメンテナンス中です」といった分かりやすい日本語のページを表示することで、利用者の不安を解消します。
2つ目は、セキュリティの確保です。標準のエラー画面には、サーバーの構成やプログラムの変数名、データベースの構造など、悪意のある攻撃者にヒントを与えてしまう情報が含まれていることがあります。これらを隠すことは、Webサイトを守るための基本中の基本です。
3つ目は、運用管理の効率化です。エラーが発生したことをログに記録したり、管理者に通知したりする処理を共通化することで、問題の早期発見と解決につなげることができます。サーブレットでは、各プログラムの中に個別にエラー処理を書くのではなく、サーバー全体で一括してエラー画面を切り替える仕組みが用意されています。
まずは、プログラム内でエラーを意図的に発生させて、どのように制御されるかを確認するためのJavaコードを見てみましょう。
package com.example;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 意図的にエラーを発生させるサーブレット
*/
@WebServlet("/ErrorDemoServlet")
public class ErrorDemoServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 0で割り算をしてArithmeticExceptionを発生させる
int result = 10 / 0;
// 通常の出力(ここは実行されません)
response.getWriter().println("計算結果: " + result);
}
}
上記のコードを実行すると、コンソールにはエラー内容が表示され、ブラウザには「HTTP Status 500」というエラーメッセージが返されます。これをどのように「自分専用のページ」に誘導するかが、これからの学習のポイントです。
2. web.xmlやアノテーションによる例外指定の仕組み
JavaのWebアプリケーションでは、特定のエラーが発生したときにどのページを表示するかを、設定ファイルであるweb.xmlに記述するのが一般的です。これを「エラーページマッピング」と呼びます。
設定は非常にシンプルです。<error-page>タグの中に、「どんなエラーのときに(エラーコードや例外クラス名)」と「どのページを表示するか(場所)」を書くだけです。具体的な書き方を確認してみましょう。
<!-- web.xml の設定例 -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0">
<!-- 特定のエラーコード(404: ページが見つからない)に対する設定 -->
<error-page>
<error-code>404</error-code>
<location>/errors/not-found.jsp</location>
</error-page>
<!-- 特定の例外クラス(Javaのプログラムエラー)に対する設定 -->
<error-page>
<exception-type>java.lang.ArithmeticException</exception-type>
<location>/errors/calc-error.jsp</location>
</error-page>
<!-- 全ての500系エラー(サーバー内部エラー)をまとめる設定 -->
<error-page>
<error-code>500</error-code>
<location>/errors/server-error.jsp</location>
</error-page>
</web-app>
- error-code:HTTPレスポンスの状態コードで指定します(404や500など)。
- exception-type:Javaのクラス名で指定します。特定のプログラムミスにだけ反応させたい場合に便利です。
最近のJava(Servlet 3.0以降)では、アノテーションを使った設定も増えていますが、エラーページに関しては一元管理しやすいweb.xmlでの設定が現在も主流です。ただし、サーブレットクラス自体で個別に例外をキャッチして処理を分岐させることも可能です。
例えば、以下のコードは「try-catch」という構文を使い、エラーが発生しても画面を止めずに、自力でエラー専用ページへ転送(フォワード)する例です。
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
// データの登録処理など
performDatabaseUpdate(request);
} catch (Exception e) {
// エラーが発生した場合は、エラーメッセージをセットしてJSPへ転送
request.setAttribute("errorMessage", "データの保存に失敗しました。時間をおいてやり直してください。");
request.getRequestDispatcher("/error_message.jsp").forward(request, response);
}
}
このように、「システム全体の共通エラー」はweb.xmlで、「特定の入力ミスなどの個別エラー」はJavaプログラム内で、と使い分けるのがプロの設計です。初心者のうちは、まずはweb.xmlによる一括設定からマスターしましょう。これだけで、Webアプリの完成度が格段に上がります。
3. エラーページ(500/404)をカスタマイズする重要性
Webサイトを閲覧していて、「404 Not Found」や「500 Internal Server Error」という無機質な白い画面を見たことはありませんか?これらはブラウザやWebサーバーが標準で持っているエラー画面ですが、商用のサービスでこれをそのまま出すのは避けるべきです。
なぜカスタマイズが必要なのか、その具体的なメリットを深掘りしてみましょう。
URLの打ち間違いや、記事が削除されたときに発生します。カスタマイズすることで、トップページへのリンクや検索窓を表示し、ユーザーを迷わせない工夫ができます。
プログラムの不具合やDB接続エラーなどで発生します。丁寧な謝罪メッセージと「再読み込み」を促すボタンなどを設置し、不快感を軽減させることが重要です。
カスタマイズされたエラーページを作る際のポイントは、デザインの統一感です。エラーが起きた瞬間にサイトのデザインがガラッと変わってしまうと、ユーザーは「別の怪しいサイトに飛ばされたのではないか」と不安になります。ヘッダーやフッター、フォントなどは通常のページと同じものを使用しましょう。
また、開発者向けの情報(スタックトレースと呼ばれるエラーの履歴など)は、エラーページに表示してはいけません。以下の出力結果のような、プログラムの内部構造が丸見えの状態は非常に危険です。
java.lang.NullPointerException
at com.example.MyService.doProcess(MyService.java:42)
at com.example.MyServlet.doGet(MyServlet.java:15)
...
こうした情報は、ユーザーに見せるのではなく、サーバー上の「ログファイル」にこっそり保存するように設定します。Javaの標準機能を使えば、ユーザーには優しいメッセージを、管理者には詳細なエラーログを、といった「情報の切り分け」が簡単に行えます。これにより、プロフェッショナルで信頼性の高いWebアプリケーションへと一歩近づくのです。
最後に、エラーページを表示するためのシンプルなJSP(HTMLのようなファイル)の例を紹介します。Bootstrapを使えば、初心者でも数分で見栄えの良いエラー画面が作れますよ。
<%-- errors/server-error.jsp --%>
<%@ page contentType="text/html; charset=UTF-8" isErrorPage="true" %>
<!DOCTYPE html>
<html>
<head>
<title>システムエラー | Javaプログラミング講座</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="text-center p-5 shadow bg-white rounded">
<h1 class="display-4 text-danger">申し訳ございません</h1>
<p class="lead">システム内部で予期せぬエラーが発生しました。</p>
<hr class="my-4">
<p>しばらく時間をおいてから、再度お試しいただくか、管理者へお問い合わせください。</p>
<a class="btn btn-primary btn-lg" href="/index.jsp" role="button">トップページへ戻る</a>
</div>
</div>
</body>
</html>
このように、「エラーは必ず起こるもの」という前提で準備をしておくことが、優れたWebエンジニアへの第一歩です。サーブレットにおけるエラーハンドリングの基本をマスターして、ユーザーに優しいシステム作りを目指しましょう!
新人
「先輩、さっき教えてもらったweb.xmlの設定はすごく便利ですね!でも、特定の処理の中で、もっと細かくエラーの内容によって画面を切り分けたいときはどうすればいいんでしょうか?」
先輩
「いい質問ですね。システム全体の共通エラーは設定ファイルに任せるのが正解ですが、業務ロジックの中で発生するエラーはJavaのプログラム側で制御する必要があります。そこで重要になるのが、try-catchによる例外捕捉と、適切な『ステータスコード』の返却ですよ。」
新人
「ステータスコード……404とか500とかの数字のことですよね。プログラムから自分で送れるんですか?」
先輩
「その通りです。これを使うことで、ブラウザや上位のシステムに対して『何が原因でエラーになったのか』を正確に伝えることができるんです。具体的な実装方法を見ていきましょう。」
4. try-catchによる例外捕捉とステータスコード制御
Javaサーブレットの開発において、実行時に発生する可能性のあるエラーをあらかじめ予測し、プログラムが強制終了しないように囲い込む仕組みがtry-catch文です。例外をキャッチすることで、単にエラー画面を表示するだけでなく、データを元の状態に戻す(ロールバック)処理や、特定の条件に基づいたエラーレスポンスの返却が可能になります。
Webアプリケーションにおいて特に重要なのが、HttpServletResponseオブジェクトを使用して、適切なHTTPステータスコードを設定することです。例えば、ユーザーの入力値が正しくない場合は「400 Bad Request」、権限がない場合は「403 Forbidden」を返すのが一般的です。これにより、フロントエンド側でのエラーハンドリングもスムーズになります。
すべてのエラーを「200 OK(成功)」で返して、画面の中身だけエラーメッセージにするのはアンチパターンです。検索エンジンのクローラーや監視システムは、ステータスコードを見てサイトの健康状態を判断するため、適切なコードを返すことはSEOやシステム運用において極めて重要です。
以下のサンプルコードでは、ユーザーIDを検索する処理において、データが見つからない場合や入力不備がある場合に、個別のステータスコードを返却する方法を示しています。
package com.example.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/UserSearchServlet")
public class UserSearchServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String userId = request.getParameter("id");
try {
if (userId == null || userId.isEmpty()) {
// 不正なリクエストとして400エラーを返す
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "ユーザーIDが指定されていません。");
return;
}
// ユーザー検索処理(模擬)
boolean found = searchUser(userId);
if (!found) {
// データが見つからない場合は404エラーを返す
response.sendError(HttpServletResponse.SC_NOT_FOUND, "指定されたユーザーは見つかりませんでした。");
return;
}
response.getWriter().println("ユーザーID: " + userId + " の情報を表示します。");
} catch (NumberFormatException e) {
// 数値形式エラーの場合は400を返す
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "IDは数値で指定してください。");
} catch (Exception e) {
// 予期せぬ内部エラーは500を返す
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "システムエラーが発生しました。");
}
}
private boolean searchUser(String id) throws Exception {
// 本来はDBアクセスなどを行う
if ("999".equals(id)) throw new Exception("DB接続失敗");
return "123".equals(id);
}
}
このように、response.sendError()メソッドを使用すると、サーブレットコンテナに対してエラーが発生したことを通知できます。この通知を受けると、先ほど学習したweb.xmlのエラーページ設定が自動的に呼び出され、ユーザーには用意しておいた綺麗なデザインのJSPが表示される、という連携が生まれます。
5. ログ出力(Logger)の基本設定と適切なログレベル
エラーが発生した際、ユーザーには優しい言葉で案内を出しつつ、開発者はその裏で「何が起きたのか」を正確に把握しなければなりません。そのために欠かせないのがログ出力です。初心者のうちはSystem.out.println()を使いがちですが、実務では必ず専用のロギングライブラリ(Java標準のjava.util.loggingや、SLF4J, Log4j2など)を使用します。
ログ出力において最も重要な考え方はログレベルの使い分けです。すべての情報を一律に出力してしまうと、大量のログの中に重要なエラー情報が埋もれてしまい、障害発生時の調査が困難になります。一般的に使用される主要なログレベルは以下の通りです。
| ログレベル | 概要と使い分けの基準 |
|---|---|
| ERROR | システムの継続に支障が出る重大な問題。即座に管理者が対応すべき事態。 |
| WARN | 現在は動作しているが、放置すると将来的に問題になる可能性がある警告。 |
| INFO | システムの起動、停止、ユーザーのログインなど、正常な動作の記録。 |
| DEBUG | 開発中のデバッグ用。変数の値の変化など、詳細な動作確認のための情報。 |
それでは、Java標準のロガー(Logger)を使った実装例を見てみましょう。サーブレットでどのようにログを記録し、例外情報を保存するかを確認してください。
package com.example.servlet;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/LogDemoServlet")
public class LogDemoServlet extends HttpServlet {
// クラス名に基づいたロガーの生成
private static final Logger logger = Logger.getLogger(LogDemoServlet.class.getName());
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
logger.log(Level.INFO, "ログイン処理を開始します。");
try {
String password = request.getParameter("password");
if (password == null) {
// 警告ログの出力
logger.log(Level.WARNING, "パスワードが未入力の状態でアクセスされました。");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 認証処理(模擬)
authenticate(password);
logger.log(Level.INFO, "ログインに成功しました。");
} catch (SecurityException e) {
// エラーログの出力(例外オブジェクトを渡すことで詳細を記録)
logger.log(Level.SEVERE, "セキュリティ違反を検知しました。", e);
response.sendError(HttpServletResponse.SC_FORBIDDEN);
} catch (Exception e) {
// 重大なエラーの記録
logger.log(Level.SEVERE, "予期せぬエラーにより処理が中断されました。", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private void authenticate(String password) throws Exception {
if ("hack".equals(password)) throw new SecurityException("不正アクセス");
if ("error".equals(password)) throw new Exception("DBエラー");
}
}
上記のコードのポイントは、logger.log(Level.SEVERE, "メッセージ", e);のように、第3引数に例外オブジェクト(e)をそのまま渡している点です。これにより、後述するスタックトレースがログファイルに記録され、原因究明が劇的に楽になります。
6. 運用保守で役立つスタックトレースの出力方法
システム開発や運用保守の現場で、エンジニアが最も頼りにするのがスタックトレース(Stack Trace)です。これは、プログラムが実行されていた「足跡」のようなもので、エラーが発生した瞬間に、どのクラスの、何行目で、どのメソッドが呼ばれていたかを時系列で逆順に表示したものです。
例えば、NullPointerExceptionが発生した際、スタックトレースがあれば「どの変数がnullだったのか」を特定する強力な手がかりになります。しかし、これを直接ブラウザに表示してしまうと、プログラムのソースコードの構造が外部に漏洩し、脆弱性につながるため、必ず「ログファイル」へ出力するように設定します。
スタックトレースの読み方(例)
java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at com.example.service.UserService.validate(UserService.java:45)
at com.example.servlet.UserServlet.doPost(UserServlet.java:28)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681)
...
この例では、まずUserService.javaの45行目でエラーが起きたことがわかります。そして、それを呼び出したのはUserServlet.javaの28行目であるという流れが一目で把握できます。
サーブレット開発において、スタックトレースを適切に管理するための実装パターンを紹介します。ポイントは、キャッチした例外を握りつぶさず、必ず記録することです。
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
// 何らかの業務処理
executeBusinessLogic();
} catch (Exception e) {
// 【良い例】ログにスタックトレースを出力する
logger.log(Level.SEVERE, "障害が発生しました。調査が必要です。", e);
// ユーザーにはカスタマイズしたエラー画面を見せる
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
private void executeBusinessLogic() {
String data = null;
System.out.println(data.toUpperCase()); // ここでNullPointerExceptionが発生
}
catch (Exception e) { }のように、キャッチした後に何も処理を書かない、あるいはe.printStackTrace()を標準出力に流すだけでログに残さない手法は避けましょう。これを行うと、本番環境で不具合が起きた際に「原因が全く分からない」という最悪の状態に陥ります。
また、大規模なシステムでは、エラー時に発行された一意の「エラーID」を画面に表示し、同時にログにもそのIDを記録する手法が取られます。ユーザーから「エラーID:ABC-123が出ました」と連絡をもらうことで、管理者はログファイルからそのIDを検索し、即座に該当のスタックトレースを見つけ出すことができるのです。
7. 実践的なエラーハンドリングの設計指針
これまでに学んだ内容を統合して、プロレベルのWebアプリにおけるエラーハンドリングの設計方針を整理しましょう。ポイントは「誰に」「何を」見せるかを明確に分けることです。これを情報の階層化と呼びます。
まず、一般ユーザーに対しては、専門用語を一切排除した「お詫びと次のアクション」を提示します。「DB接続がタイムアウトしました」ではなく「現在混み合っております。しばらく経ってから再度お試しください」といった表現です。ここにはサイトのロゴやナビゲーションメニューを含め、サイトから離脱されない工夫を凝らします。
次に、運用担当者・開発者に対しては、サーバーログを通じて「完全な文脈」を提供します。発生時刻、操作していたユーザーのID、リクエストされたURL、パラメータ、そしてスタックトレース。これらが揃って初めて、迅速なトラブルシューティングが可能になります。
セキュリティ
サーバー内部情報やミドルウェアのバージョン、プログラムの行数などを外部に漏らさない。
追跡可能性
いつ、どこで、なぜエラーが起きたかを、ログレベルを使い分けて正確に記録する。
親切な誘導
エラーは行き止まりではなく、次のステップ(戻るボタンや検索窓)への案内板であるべき。
Javaサーブレットという歴史ある技術を学ぶことは、Web開発の根幹にある「リクエストとレスポンス」の仕組みを深く理解することに繋がります。エラーハンドリングはその中でも、システムの堅牢性を左右する最も重要なパーツの一つです。今回学んだtry-catch、web.xml、そしてLoggerを駆使して、ユーザーにも開発者にも優しい、一流のアプリケーションを目指してコードを書いていきましょう。
新人
「先輩、エラーハンドリングの重要性はよく分かりました。でも、ただエラー画面を出すだけじゃなくて、ユーザーに『何が原因で、どうすればいいか』を具体的に伝えるには、セキュリティとのバランスが難しそうです。具体的にどんな点に注意すればいいんでしょうか?」
先輩
「その通り。親切心でエラーの詳細を表示しすぎると、攻撃者にサーバーの内部構造を教えてしまうことになります。ユーザーへの通知には『見せて良い情報』と『隠すべき情報』の明確な境界線があるんですよ。次は、そのセキュリティ上の注意点と具体的な伝え方を深掘りしてみましょう。」
7. ユーザーへのエラー通知とセキュリティ上の注意点
Webアプリケーションにおいて、エラーが発生したことをユーザーに伝える作業は非常にデリケートです。初心者が陥りやすい最大の罠は、プログラムが吐き出したエラーメッセージをそのまま画面に表示してしまうことです。これは利便性の観点からも、セキュリティの観点からも絶対に避けるべき行為です。
情報漏洩のリスクを最小限に抑えるためには、具体的な例外クラス名やメソッド名、さらにはデータベースのテーブル名やカラム名といった情報を、エンドユーザー向けの画面には一切出さないように設計します。例えば、データベース接続に失敗した場合に「Connection Timeout at localhost:3306」と表示すれば、攻撃者はサーバーのポート番号やデータベースの所在を知ることができてしまいます。代わりに「一時的にデータにアクセスできません」という抽象的な表現に留めるのが鉄則です。
SQL文の内容や、Javaのコンパイルエラーの内容を直接表示してはいけません。これらは「情報漏洩」とみなされ、脆弱性診断においても重大な指摘事項となります。
一方で、ユーザーへの通知内容は、不安を煽らない言葉選びが求められます。エラーが発生した原因が「システム側」にあるのか「ユーザーの入力」にあるのかを切り分け、適切なアクションを促しましょう。入力ミスであれば「赤文字で修正箇所を指摘」し、システム障害であれば「お詫びと復旧の見込み」を伝えます。このように、エラーメッセージを「システムからの冷たい拒絶」ではなく「解決のためのガイド」としてデザインすることが、高品質なWebアプリの証です。
以下に、セキュリティに配慮しつつ、ユーザーの入力状況に応じてメッセージを動的に切り替えるサーブレットの実装例を紹介します。ここでは、エラーの詳細はログに記録し、ユーザーには安全で分かりやすいメッセージのみを返却しています。
package com.example;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/SecureErrorServlet")
public class SecureErrorServlet extends HttpServlet {
private static final Logger logger = Logger.getLogger(SecureErrorServlet.class.getName());
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
String inputData = request.getParameter("data");
try {
// データの検証処理
if (inputData == null || inputData.trim().isEmpty()) {
// ユーザー起因のエラー:安全なメッセージをリクエスト属性にセット
request.setAttribute("safeMessage", "入力内容が空です。正しく入力してください。");
request.getRequestDispatcher("/WEB-INF/jsp/input_error.jsp").forward(request, response);
return;
}
// 業務ロジックの実行(ここで例外が発生する可能性がある)
processBusinessLogic(inputData);
response.sendRedirect("success.jsp");
} catch (IllegalArgumentException e) {
// 入力不備による例外をキャッチ
logger.log(Level.INFO, "バリデーションエラーが発生しました: " + e.getMessage());
request.setAttribute("safeMessage", "入力された形式に誤りがあります。");
request.getRequestDispatcher("/WEB-INF/jsp/input_error.jsp").forward(request, response);
} catch (Exception e) {
// システム内部エラー:詳細はログに出力し、ユーザーには隠蔽する
logger.log(Level.SEVERE, "システム内部で未定義の例外が発生しました。入力値: " + inputData, e);
// セキュリティに配慮した汎用的なエラーメッセージ
request.setAttribute("safeMessage", "申し訳ございません。現在システムをご利用いただけません。");
request.getRequestDispatcher("/WEB-INF/jsp/system_error.jsp").forward(request, response);
}
}
private void processBusinessLogic(String data) throws Exception {
if ("error".equals(data)) {
throw new Exception("内部データベース接続エラー");
}
}
}
8. 実務で使える共通エラーハンドリングの実装パターン
実際の現場では、すべてのサーブレットに個別にエラー処理を書くのは非効率であり、コードの重複を招きます。そこで、共通エラーハンドリングという設計手法を取り入れます。これは、アプリケーション全体で発生するエラーを、特定のクラスやフィルターで一括して受け止める仕組みです。
代表的なパターンの一つが、Servlet Filter(フィルター)を利用した実装です。フィルターは、リクエストがサーブレットに届く前、およびレスポンスがクライアントに返る前のタイミングで処理を割り込ませることができます。すべてのリクエストをtry-catchで囲むフィルターを作成しておけば、個別のサーブレットで万が一例外のキャッチ漏れがあっても、共通の処理で安全に拾い上げることができます。
もう一つのパターンは、基底クラス(BaseServlet)の活用です。自作の全てのサーブレットが共通の親クラスを継承するようにし、その親クラスで共通のエラーログ出力や、エラー画面へのフォワード処理を定義しておきます。これにより、開発者は「エラーが起きたらどうするか」を都度考える必要がなくなり、本来の業務ロジックの実装に集中できるようになります。
以下に、サーブレットフィルターを用いた共通エラーハンドリングの構成例を示します。このフィルターを通るすべての処理に対して、統一されたログ出力とエラーページ表示を強制することができます。
package com.example.filter;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
@WebFilter("/*") // すべてのリクエストに対して適用
public class GlobalErrorFilter implements Filter {
private static final Logger logger = Logger.getLogger(GlobalErrorFilter.class.getName());
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
// 次の処理(サーブレットなど)を実行
chain.doFilter(request, response);
} catch (Throwable t) {
// アプリケーション内のあらゆる未キャッチ例外をここで捕捉
logger.log(Level.SEVERE, "共通フィルターで予期せぬエラーを捕捉しました。", t);
// レスポンスが既にコミット(送信開始)されていなければエラーページへ
if (!response.isCommitted()) {
if (response instanceof HttpServletResponse) {
((HttpServletResponse) response).sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
}
public void init(FilterConfig filterConfig) throws ServletException {}
public void destroy() {}
}
このように共通化することで、メンテナンス性の向上という大きな恩恵を受けられます。例えば、エラーログの出力先をデータベースに変更したい場合や、通知メールを送信する機能を追加したい場合、各サーブレットを修正することなく、フィルター一箇所を書き換えるだけで対応が完了します。これは大規模開発において極めて重要な設計思想です。
9. Servletのエラー処理とログ出力のポイント整理
これまでの内容を踏まえ、Javaサーブレットにおけるエラー処理とログ出力の要点を整理しましょう。ここでの「ポイント」は、単にコードが動くことではなく、運用の現場でエンジニアが苦労しないためのノウハウです。エラーハンドリングは「予防」であり、ログ出力は「証拠保全」であることを意識してください。
まず、エラー処理においては「適切な粒度での例外キャッチ」が重要です。何でもかんでもExceptionでまとめてキャッチするのではなく、ファイル操作ならIOException、数値変換ならNumberFormatExceptionというように、具体的な例外クラスで受けることで、エラーの原因に合わせたきめ細やかな対応(リトライ処理や、ユーザーへの説明の出し分け)が可能になります。
ログ出力に関しては、コンテキスト情報(状況証拠)を豊富に含めることが不可欠です。単に「エラーが発生しました」というログだけでは、後から調査する際に何も分かりません。エラーが発生した時にログインしていた「ユーザーID」、どのような「リクエストパラメータ」を受け取っていたか、そしてどの「セッション状態」だったのか。これらの情報をスタックトレースと共に記録することで、再現の難しい不具合の原因を特定するスピードが劇的に向上します。
実装時に必ずチェックすべきリスト
web.xmlに404と500のエラーページマッピングが正しく設定されているか。- 重要なエラーログにはスタックトレースが必ず含まれているか。
- ユーザー向けのメッセージにシステム内部の機密情報(パスワードやDB構造)が含まれていないか。
finallyブロックや、Java 7以降のtry-with-resourcesを使用して、ファイルやDB接続のクローズ漏れがないか。- 開発環境と本番環境でログレベルの切り替えができるようになっているか。
最後に、エラーハンドリングはテスト工程において特に注意深く確認する必要があります。正常系のテスト(正しく動くことの確認)は誰でも行いますが、異常系のテスト(わざとエラーを起こして正しく処理されるかの確認)は疎かになりがちです。存在しないページにアクセスして404画面が出るか、不正なデータを送って適切なメッセージが出るか、といった「負のシナリオ」を網羅することで、システムの堅牢性は本物になります。
Javaサーブレットという基礎を学ぶことで、Spring Bootなどのモダンなフレームワークを使った開発においても、その裏側で何が起きているかを正しく想像できるようになります。堅牢なエラーハンドリングができるエンジニアは、チームや顧客から厚い信頼を寄せられる存在になれるでしょう。今回学んだ技術を、ぜひ明日からのコーディングに活かしてください。