Java Servletの例外処理(try-catch)を徹底解説!エラーに強いWebアプリの作り方
新人
「先輩、Java Servletでプログラムを作っている最中に、画面が真っ白になったり、英語の長いエラーメッセージが出たりして困っています。これってどうすれば防げるんですか?」
先輩
「それは『例外』が発生している証拠ですね。Java Servletの世界では、予期せぬトラブルが起きたときのために『例外処理』という仕組みを組み込んでおく必要があるんですよ。」
新人
「例外処理……。具体的にどうやって書けば、ユーザーに優しいエラー画面を出せるようになるんでしょうか?」
先輩
「基本となるのはtry-catchという書き方です。まずは、なぜエラーハンドリングが大切なのか、その基本から一緒に確認していきましょう!」
1. Java Servletにおける例外処理の基本(try-catchの役割)
Javaプログラミング、特にWebアプリケーション開発において「例外(Exception)」とは、プログラムの実行中に発生する「予期せぬトラブル」のことを指します。例えば、数字を入力すべき場所に文字が入っていたり、接続先のデータベースが止まっていたりする場合です。
これらのトラブルが発生したときに、プログラムが突然終了してしまわないように守る仕組みが「try-catch(トライ・キャッチ)」です。初心者の方にもわかりやすく、日常生活の例えで説明しましょう。
身近な例え:お料理中のトラブル
「カレーを作る(try)」という動作を想定してください。もし「ジャガイモが切れていた(例外)」としても、そこで料理を完全に投げ出すのではなく、「代わりにサツマイモを使う(catch)」という対策を立てておけば、美味しい夕飯を完成させることができますよね。これがプログラミングにおける例外処理の考え方です。
Java Servletのコード内では、以下のような構文を使ってトラブルを未然に防ぎます。
try {
// 1. ここに本来実行したい処理を書く(トラブルが起きるかもしれない処理)
System.out.println("データベースに接続します");
} catch (Exception e) {
// 2. トラブルが発生したときの「身代わり」の処理を書く
System.out.println("エラーが発生しました。管理者にお問い合わせください。");
}
このtryブロックの中で何か不具合が起きると、即座にcatchブロックへと処理がジャンプします。これにより、Webサイト全体がクラッシュして止まってしまう最悪の事態を回避できるのです。Webアプリ開発者にとって、このガードレールを作る作業は非常に重要なスキルとなります。
2. Webアプリケーションでエラーハンドリングが必要な理由
なぜ普通のデスクトップアプリ以上に、Webアプリケーション(Java Servlet)ではエラーハンドリング(例外処理)が厳しく求められるのでしょうか。それには、Web特有の「不特定多数が利用する」という性質が関係しています。
適切な処理をしていないと、ブラウザ上にプログラムの内部構造やエラー内容(スタックトレース)が丸見えになってしまいます。これは悪意のある攻撃者に「このシステムはこの部分が弱点だ」とヒントを与えてしまうようなものです。
利用者が「送信ボタン」を押した後に、真っ白な画面や英語の羅列が表示されたらどう思うでしょうか。「壊してしまったかも」と不安にさせてしまいます。適切なハンドリングがあれば、「現在メンテナンス中です」などの丁寧な案内が出せます。
また、Java Servletはサーバー上で動いています。一人のユーザーが起こした小さなエラーが原因でサーバー全体が重くなったり、メモリを使い果たしてしまったりすると、他の正常に利用しているユーザーにも迷惑がかかります。そのため、エラーを検知して適切に「お掃除」し、システムを安定稼働させ続ける義務が開発者にはあるのです。
用語解説:ハンドリング(Handling)
直訳すると「操作する」「扱う」という意味です。ITの世界では、発生した事象に対して適切な対処を行うことを「エラーをハンドリングする」と呼びます。
3. Servletプログラムにおける例外(Exception)の発生とキャッチ
ここからは、実際のJava Servletの構造に当てはめて、どのように例外が発生し、どこでキャッチすべきかを学んでいきましょう。Servletでは、doGetやdoPostといったメソッドの中で主要な処理を書きますが、ここではネットワークエラーやデータ変換エラーなど、多種多様な問題が待ち構えています。
例えば、ユーザーが入力した「年齢」という文字列を数字に変換する処理を考えてみます。もしユーザーが「二十歳」と漢字で入力してしまった場合、変換処理で例外が発生します。
具体的なコード例:入力値チェックの例外処理
package com.example;
import java.io.IOException;
import java.io.PrintWriter;
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("/AgeCheckServlet")
public class AgeCheckServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
// 画面からの入力を受け取る
String inputAge = request.getParameter("age");
try {
// 文字列を数字に変換する(ここでエラーが起きやすい!)
int age = Integer.parseInt(inputAge);
out.println("<p>あなたの年齢は " + age + " 歳ですね。</p>");
} catch (NumberFormatException e) {
// 数字ではない文字が入力された場合の処理
out.println("<p style='color:red;'>エラー:半角数字を入力してください。</p>");
System.out.println("ログ出力:数字変換に失敗しました。入力値:" + inputAge);
}
}
}
上記のプログラムでは、Integer.parseIntという命令を使って数字への変換を試みています。しかし、文字が入ってきた場合にはNumberFormatExceptionという種類の例外が投げられます。これをcatchで捕まえることで、画面に赤い文字でエラーメッセージを表示し、プログラムを安全に継続させています。
実行結果のイメージ
(正常に入力された場合)
あなたの年齢は 25 歳ですね。
(「あ」など数字以外を入力した場合)
エラー:半角数字を入力してください。
Servletにおける例外処理のポイントは、「どこまでがtryの範囲か」を明確にすることです。何でもかんでも一つの大きなtryブロックに入れてしまうと、どこでエラーが起きたのか特定が難しくなります。意味のある単位で区切って、きめ細やかな対応(キャッチ)を行うのが、プロのエンジニアへの第一歩です。
さらに、Servlet特有の例外としてServletExceptionやIOExceptionがあります。これらはServletが動作するために不可欠な通信やファイル操作に関する例外です。これらも適切に処理し、必要に応じてエラー専用のJSPページ(エラーページ)へ転送(フォワード)するなどの工夫が現場では行われています。
4. try-catch-finally文によるリソース解放と後続処理
Java Servletの開発において、try-catchに続く重要な要素が「finally(ファイナリー)」ブロックです。これは、例外が発生したかどうかにかかわらず、必ず最後に実行される処理を記述するための場所です。特に、データベース接続や外部ファイルを開くといった「リソース」を扱う処理では、このfinallyが不可欠な役割を果たします。
先輩
「新人の君、料理の後片付けを忘れていませんか?材料が足りなくて料理が中断しても、ガスコンロの火を消して、使った道具を洗うのは絶対に必要なことですよね。プログラミングも同じで、エラーが起きても必ず『後片付け』をしなければなりません。」
新人
「なるほど。エラーが起きて途中で処理が止まると、開きっぱなしになった接続が残ってしまう可能性があるんですね。それがシステムの動作を重くする原因になるということですか?」
先輩
「その通りです。その『後片付け』を確実に行うための場所が、このfinallyブロックなんです。」
具体的なコード例:リソース解放の基本構造
以下の例では、仮想的なリソースをオープンし、エラーの有無を問わずクローズ(解放)する流れを示しています。
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
PrintWriter out = response.getWriter();
// 本来はデータベース接続などの重い処理が入ります
boolean isResourceOpen = false;
try {
isResourceOpen = true;
out.println("リソースをオープンしました。");
// 何らかの計算処理
String data = request.getParameter("data");
int result = 100 / Integer.parseInt(data); // 0入力で算術例外が発生
out.println("計算結果:" + result);
} catch (ArithmeticException e) {
out.println("エラー:0で割ることはできません。");
} catch (NumberFormatException e) {
out.println("エラー:数値を入力してください。");
} finally {
// 例外の発生有無に関わらず、必ず実行される
if (isResourceOpen) {
out.println("finallyブロック:リソースを安全に解放しました。");
}
out.close();
}
}
try-with-resourcesという現代的な書き方
Java 7以降では、AutoCloseableインターフェースを実装したクラスであれば、try-with-resources文という、より簡潔な書き方が可能です。これにより、finallyブロックで明示的にcloseを呼ばなくても、自動的にリソースが解放されます。しかし、現場の古いソースコードを保守する場合や、複雑な終了処理が必要な場合には、依然としてfinallyの概念を理解しておくことが重要です。
5. ユーザーにエラーを隠す:web.xmlによるエラーページ遷移の仕組み
どれほど丁寧にtry-catchを書いても、予想外の場所で例外が漏れてしまうことがあります。その際、TomcatなどのWebサーバーが標準で用意しているエラー画面が表示されてしまうと、スタックトレース(エラーの詳細な内部情報)が露出してしまいます。これはセキュリティ上、極めて危険な状態です。
そこで活用するのが、Webアプリケーションの設定ファイルである「web.xml」です。このファイルに設定を書き込むことで、特定のステータスコード(404エラーや500エラーなど)や特定の例外が発生した際に、自動的にユーザーを「専用のエラー告知ページ」へ誘導することができます。
web.xmlの設定例
以下の設定をweb.xmlに追記することで、エラー発生時の振る舞いを制御できます。
<web-app>
<!-- 404 Not Found(ページが見つからない)の場合 -->
<error-page>
<error-code>404</error-code>
<location>/error/404.jsp</location>
</error-page>
<!-- 500 Internal Server Error(サーバー内部エラー)の場合 -->
<error-page>
<error-code>500</error-code>
<location>/error/500.jsp</location>
</error-page>
<!-- 特定の例外クラスが発生した場合の指定も可能 -->
<error-page>
<exception-type>java.lang.NullPointerException</exception-type>
<location>/error/npe_error.jsp</location>
</error-page>
</web-app>
この仕組みを導入するメリットは、すべてのServletに個別に例外処理を書かなくても、アプリ全体で統一されたデザインのエラー画面を提供できる点にあります。ユーザーには「申し訳ございません、現在システムが混み合っております」といった親切なメッセージを見せつつ、開発者は裏側でひっそりと原因調査を行う。これがプロフェッショナルなWebアプリの振る舞いです。
注意点:エラーページのパス
locationに指定するパスは、Webアプリケーションのルート(WebContentやwebappフォルダの直下)からの相対パスで記述します。遷移先がJSPである必要はなく、HTMLファイルでも構いませんが、共通のヘッダーやフッターを表示させたい場合はJSPが便利です。
6. Servletコンテキストでのスタックトレースの見方とデバッグのコツ
ユーザーにはエラーを隠しますが、開発者である私たちはエラーの正体を見極めなければなりません。そのための最強の武器が「スタックトレース(Stack Trace)」です。これは、例外が発生した瞬間に、プログラムのどのメソッドがどの順番で呼び出されていたかを記録した、いわば「犯行現場の記録」です。
スタックトレースの読み解き方
初心者がスタックトレースを見ると、その膨大な英語の量に圧倒されてしまいがちですが、実は見るべきポイントは限られています。重要なのは「一番上の原因」と「自分が書いたクラス名」を探すことです。
java.lang.NullPointerException
at com.example.MyServlet.doGet(MyServlet.java:45)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:626)
at ...(以下、サーバー内部の処理が続く)
上記のような出力があった場合、注目すべきは「MyServlet.javaの45行目」です。そこがエラーの起点であることを示しています。それより下のjavax.servletなどはTomcat自体の動きなので、深追いする必要はほとんどありません。
効率的なデバッグのコツ
Servletでのデバッグをスムーズに進めるための、現場でよく使われるテクニックを紹介します。
標準エラー出力とログ
e.printStackTrace()を呼び出すと、Eclipseなどのコンソール画面に詳細が表示されます。商用環境では、これをファイルに保存する「ロギングライブラリ(Log4j2やSLF4Jなど)」を使うのが一般的です。
ブレークポイントの活用
IDE(開発環境)のデバッグ機能を使って、怪しい行に印(ブレークポイント)を付けましょう。プログラムを一時停止させて、その時点での変数の値を一つずつ確認するのが、最も確実な解決策です。
Servlet特有の問題として、ブラウザから送信された値が「null」になっていることに気づかず、メソッドを呼び出してNullPointerExceptionを起こすケースが非常に多いです。まずは「受け取った値が意図した通りか」をログに出力して確認する癖をつけましょう。
7. 現場で役立つ実践的な例外設計の考え方
ここまで技術的な側面を解説してきましたが、最後に応用編として「どのように例外を使い分けるか」という設計の視点をお話しします。例外には、大きく分けて二つの種類が存在します。
| 種類 | 特徴 | 代表例 |
|---|---|---|
| チェック例外 | コンパイル時に処理を記述することが強制される。 | IOException, SQLException |
| 非チェック例外 | 実行時に発生する。開発者の不注意によるものが多い。 | NullPointerException, RuntimeException |
Webアプリ開発では、ネットワーク越しにデータをやり取りするため、自分のプログラムが完璧でもエラーが起きる(チェック例外が発生する)場面が多々あります。こうした「避けられないトラブル」に対しては、必ずtry-catchで網を張り、ユーザーに適切な案内を行うようにしましょう。一方で、NullPointerExceptionなどは、事前にif文でnullチェックを行えば回避できるものです。例外処理に頼りすぎるのではなく、まずは「例外を起こさないコード」を書くことが大切です。
また、大規模な開発現場では、独自の例外クラス(カスタム例外)を作成することもあります。例えば、在庫が足りないときに投げるNoStockExceptionといったものです。これにより、エラーの原因がより明確になり、処理を分岐させやすくなります。このように、例外処理は単なる「エラー対策」ではなく、プログラムの「流れを制御する」重要なパーツなのです。
今回の振り返りポイント
- finallyを使って、データベースなどの接続を確実にクローズしよう。
- web.xmlを活用して、ユーザーに不要な情報を漏らさない工夫をしよう。
- スタックトレースは、自分の書いたソースコードの行数に注目して読もう。
- 例外処理は「ユーザーの安心」と「システムの安定」を守るための守護神である。
7. ログ出力(Logger)の重要性とエラー情報の記録方法
Webアプリケーションの運用において、プログラムが内部でどのような動きをしたか、あるいはどのようなエラーが発生したかを後から確認するための「足跡」がログ(Log)です。Java Servletの実行中に発生した例外をcatchブロックで捕まえた際、ただ画面にエラーを表示するだけでは不十分です。なぜなら、画面に表示された情報はユーザーがブラウザを閉じれば消えてしまい、開発者が後から原因を調査することができないからです。
なぜ標準出力ではダメなのか
学習段階ではSystem.out.println()を使いがちですが、実務では推奨されません。標準出力は出力先の制御が難しく、ログレベル(重要度)の管理もできないため、サーバーの性能低下を招いたり、必要な情報が埋もれたりする原因になります。
ロギングライブラリの役割
実務ではLog4j2やlogbackといったライブラリを使用します。これらを使うことで、「いつ」「どこで」「どのレベルの(INFO/WARN/ERROR)」事象が起きたのかを、日付ごとに分かれたファイルへ自動的に記録できます。
Servletにおけるログ出力の基本は、発生した例外オブジェクトそのものをロガーに渡すことです。これにより、エラーメッセージだけでなく、先ほど学んだ「スタックトレース」も丸ごとログファイルに保存されます。以下に、標準的なロギングのイメージをJavaコードで示します。
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("/LogExampleServlet")
public class LogExampleServlet extends HttpServlet {
// ロガーの初期化
private static final Logger logger = Logger.getLogger(LogExampleServlet.class.getName());
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
// 意図的にエラーを発生させる例(nullの操作)
String data = null;
data.length();
} catch (NullPointerException e) {
// エラーレベルとメッセージ、例外オブジェクトを記録する
logger.log(Level.SEVERE, "予期せぬヌルポインタ例外が発生しました。", e);
// ユーザーには安全なメッセージを返す
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "システムエラーが発生しました。");
}
}
}
ログには「何が起きたか」だけでなく、「その時、どのような入力値があったか」というコンテキスト情報を含めるのがコツです。例えば、ユーザーIDや注文番号を一緒に記録しておくことで、特定の状況下でしか発生しないバグの再現が劇的に楽になります。
8. 実務で避けるべき「握りつぶし」と適切な例外スローの作法
プログラミング初心者や、納期に追われる開発者がやってしまいがちな最大の禁忌が「例外の握りつぶし」です。握りつぶしとは、catchブロックを空にしたり、ログも出さずに処理を継続させたりすることを指します。
try {
doSomething();
} catch (Exception e) {
// 何もしない、またはコメントだけ
// TODO: 後で直す
}
これを行うと、エラーが起きたこと自体が闇に葬られます。システムは一見動いているように見えますが、内部のデータが壊れたり、後の工程で原因不明の不具合が発生したりします。この状態の調査は、砂漠で一本の針を探すような困難を極めます。
適切な例外スロー(rethrow)の考え方
Servlet単体で解決できないエラーや、呼び出し元の処理にエラーを通知すべき場合は、キャッチした例外を再度投げ直す(スローする)必要があります。これを「再スロー」と呼びます。ただし、そのまま投げるのではなく、上位の階層で扱いやすい形にラップ(包む)するのが一般的です。
public void processOrder(String orderId) throws OrderException {
try {
// データベース更新処理など
dbUpdate(orderId);
} catch (SQLException e) {
// 1. ログには詳細な原因を記録する
logger.log(Level.WARNING, "注文処理中のDBエラー: " + orderId, e);
// 2. 独自の業務例外に包んで、上位に知らせる
// 元の例外(e)を引数に渡すことで、原因の連鎖(Cause)を保持できる
throw new OrderException("注文情報の登録に失敗しました。", e);
}
}
このように、下位のメソッド(DBアクセスなど)で発生した技術的な例外を、上位のメソッド(ビジネスロジックやServlet)で「業務的な意味を持つ例外」に変換して伝えることで、プログラムの役割分担が明確になります。Servlet側では、このOrderExceptionを捕まえて、適切なJSPエラー画面へ遷移させるという判断を下せばよいのです。
9. Servletのエラー処理とログ運用のポイント整理
最後に、Servletを中心としたWebアプリケーション全体のエラー処理戦略を整理しましょう。場当たり的にtry-catchを書くのではなく、アプリケーション全体で一貫したポリシーを持つことが、保守性の高いシステムへの近道です。
- 1. 境界線でキャッチする: Servletの
doGet/doPostは、Web層とビジネスロジック層の境界です。ここで最終的な例外を捕まえ、ユーザーへのレスポンス(エラー画面)を決定します。 - 2. 共通のエラーハンドラーを作る: すべてのServletに同じような例外処理を書くのは非効率です。
BaseServletを作って共通処理をまとめたり、Filter機能を使って一括で例外をハンドリングしたりする手法を検討しましょう。 - 3. ログの保存期間を定める: ログファイルは放っておくとディスク容量を圧迫します。1ヶ月分は保持する、古いものは圧縮して別サーバーへ移すといった「ログローテーション」の設計も運用の現場では必須です。
- 4. 監視ツールとの連携:
Level.SEVERE(重大なエラー)が記録された場合に、管理者へメールやチャットで通知が飛ぶ仕組みを構築することで、ユーザーからの問い合わせ前に障害を検知できます。
エラー発生時の理想的な情報の流れ
システムで異常が発生した際、情報は以下のように二分されて流れるべきです。
「エラーページ」
安心感を与える説明と、トップページへのリンクを提供。内部情報は一切出さない。
「ログファイル」
発生時刻、スタックトレース、入力パラメータ、ユーザーIDなど、再現に必要な情報を網羅。
Java Servletによる開発は、多人数が同時にアクセスし、ネットワークやデータベースといった自分たちで制御しきれない外部要因と戦う作業でもあります。例外処理を完璧にマスターするということは、単にエラーを消すことではなく、「何かが起きたときに、何が起きたかを即座に把握し、被害を最小限に食い止める」ための守備力を身につけることなのです。この記事で学んだ基礎を活かし、エラーに負けない強固なWebアプリケーションを構築してください。
新人教育を担当する皆さんへ
新人の方には「コードは動けば良いのではなく、壊れたときにどう振る舞うかがプロの品質だ」と伝えてあげてください。例外処理の丁寧な実装は、将来の自分やチームメンバーへの最大の贈り物になります。