はじめに
システム開発において例外処理は重要なポイントですが、あまりに軽視されているのが現状ではないでしょうか。本稿では、これまでの著者の開発経験の中から培った汎用的な手法を説明します。
この記事は「美しい設計」ではなく「現実的な設計」、現場に適用できる「できるだけ手間の少なく、汎用的な設計」を目指しています。
対象読者
J2EE開発者・アーキテクト。特に業務システムの開発現場の方が対象です。
必要な環境
概念の説明が中心ですので、開発環境は必要ありません。
エラーの分類
実装時に考慮すべきエラーは2つに大別できます。
- 想定内でトランザクションの実行開始前にチェックするエラー。主に入力エラー。
- 異常な状態としてトランザクションの続行が不可能なエラー(例外)。
前者については、例外を使うべきではありません。入力チェックエラーを表現するには、ステータスコードを使うべきです。理由は次のとおりです。
- 主にUIが絡むので、catch句にUI処理を書くことになってしまう。catch句には業務処理を書くべきではない。
- 例外は処理コストが高いので、正常処理に組み込むべきではない。
- 入力エラーは複数の発生原因を返す場合があるが、例外は一つの発生原因しか返せない。例外クラスを拡張すれば可能だが、例外処理を複雑化すべきではない。
本稿で扱うのは、後者です。
例外が発生する場合とは、例えば他システムとのデータ交換において、フォーマットチェックに引っかかった場合などです。このような例外は、開発者が任意で例外を生成しthrowします。このような例外にはユーザー定義例外を使うことで、例外処理を統一した形式で行えます。
例外クラスを定義する
APIで定義されている例外は、以下の属性しか持ちません。
- メッセージ
- スタックトレース
しかしシステム開発では、運用監視に必要なメッセージコード、ログレベルも必須でしょう。そこで新たにユーザー定義クラスを作成します。
RuntimeException
を継承したクラスは、メソッドのthrowsを記述する必要がありません。例外を一覧する以外には役に立たないthrowsが無くなることで、ソースが非常にすっきりします。
ほとんどの場合はこのユーザー定義例外クラス一つで、事足ります。もし属性を追加したい場合は、ユーザー定義例外のサブクラスを作るとよいでしょう。
例外処理を基底クラスにまとめる
例外処理となるtry~catchは、処理の実行を呼び出す「根っこ」の部分のみに実装します。一般的なJ2EEフレームワークを考えてみましょう。
階層 | 分類 | 呼び出し履歴 | 説明 |
1 | サーブレット | XXXServlet.doPost() | HTTPリクエストを処理する入り口。 |
2 | コントローラ | BLInvoker.invoke() | フレームワークが、リクエストパラメタから処理すべきロジックを選択し、実行する。MVCモデルのControllerにあたる。 |
3 | 業務ロジックの基底クラス | BlBase.execute() | 業務ロジックすべてに共通する前処理、後処理、例外処理(try ~ catch)、ロギングを行う。MVCモデルのModelにあたる。 |
4 | 業務ロジック | BlCalc.execute() | ユーザー定義の業務ロジック。 |
5 | 汎用処理 | CheckUtils.isValidData() | ユーザー定義の汎用処理。 |
業務ロジックや汎用処理で発生する例外は、まとめて基底クラスで処理します。これにより、各業務ロジック内で個別に例外処理を実装する必要はなくなります。また、例外処理が基底クラスにまとまることで、ロギングもまとまります。
ロギングのポイント
一般的にログを出力するポイントは、次のとおりです。
出力内容 | 基底クラス内のロギング箇所 |
業務ロジックの開始メッセージ | try句の先頭 |
業務ロジック内で発生した例外のメッセージ | catch句 |
業務ロジックの終了メッセージ | finally句 |
ロギングも基底クラスにまとまるので、業務ロジック内で開始メッセージなどをロギングする必要はありません。
ログに出力すべき内容
ログに出力すべき項目は次のとおりです。
項目 | 説明 |
時刻 | yyyy/MM/dd hh:MM:ss.SSS形式で、西暦からミリ秒まで。実行時間を知りたい場合、ミリ秒は必須です。. |
スレッド番号 | スレッド固有の番号。マルチスレッドアプリケーションでは必須です。 |
ユーザーID | システムの利用者番号。複数ユーザーのアプリケーションでは必須です。 |
ログレベル | ログレベルは実装段階、運用段階でログの出力量を制限するために設定します。 Log4jの場合:fatal, error, warn, info, debug, trace |
メッセージコード | ログ一行毎の分類コード、もしくはエラーコード。 |
任意メッセージ | エラーメッセージなど、日本語の説明。 |
スタックトレース | 例外発生箇所までの呼び出し階層。 |
マルチスレッド・複数ユーザーのシステム(特にWebアプリ)では、スレッド番号とユーザーIDは必ず出力してください。これがなければ、ユーザーごとの処理をログで追うことができません。
以上の項目(スタックトレースを除く)を、1つのログファイルに出力します。スタックトレースは標準エラー出力に出力します。
最後に
ここまでで、どのような感想をお持ちでしょうか? よく考えて設計を行う開発者なら、「なんだ、当たり前のことばかりじゃないか」と思うでしょう。ですが、著者の経験では、こんなことがありました。
- ロジックの中にtry ~ catchが散りばめられ、例外処理が統一されていない。ログに例外を出力しない、誤ったフォーマットで出力する、一度の例外で複数回スタックトレースを出力する、など。
- すべてのクラスに開始・終了メッセージのロギングが実装され、大量にログを出力する。かつ、パフォーマンスが非常に悪い。
- ログにユーザーIDが出力されないので、誰の操作でエラーが発生したのかわからない。
こんな初歩的な問題が、実はかなりの開発現場で発生しています。当たり前のことなのに意外とまとまった資料が無かったので、まとめて公開することにしました。
例外処理とロギングは、設計・開発段階では手を抜きがちですが、ここのデキ次第で、運用開始後の障害の収束スピードと安定性が変わってきます。本稿で、サーバ管理者の睡眠時間を少しでも確保できれば、幸いです。
質問と回答
皆様から寄せられた質問に回答したいと思います。
No. | Q | A |
1 | 「例外処理・ロギングを基底クラスにまとめる」とのことですが、最近のJ2EEでは基底クラスに処理をまとめるよりもAOPを使って処理を挟み込むことが主流になっていたのではないでしょうか? | AOPを使えるならば、という前提でおっしゃる通りだと思います。ただし、実際にはAOPを現場の開発者が使えることは少ないのが現状です。AOPを使うためのライブラリはフリーのものが多く、フリーというだけで使用を拒否されます。また、AOPを理解している人ばかりではありません。AOPを使っていいですか?」と聞いても「そんなわけのわからないもの使うな。」という反応が返るでしょう。 |
2 | ロギングの終了処理をfinally句に記述する異常発生時にも正常に終了したかのように見えないでしょうか。私が設計するのであれば正常終了はreturn文が実行された後に実行するようにAOPを使って実装します。 | どこで処理が落ちたかはスタックトレースからわかります。finally句で必ず出すようにしない場合、明確に”処理を出た”ことがわかりにくいと思いますが、好みの問題かもしれません。 |
3 | 標準エラー出力に出力するのはなぜですか?あらかじめファイルに蓄積したほうが後で調査しやすくないですか?標準エラー出力されたものは設定によってはそのままコンソールに表示されます。コンソールに出力されてしまったらどうやって原因を調査するのですか? | 標準エラー出力は独立したファイルに出力される、というのが大前提です。標準エラー出力がコンソールのまま、という業務システムはありえない、必ず独立したファイルに出力される、と考えています。標準エラー出力はJavaで作られたシステム全てに用意されているので、設計を統一できるという点で有利です。出力フォーマットは、System.setErr()でカスタマイズしたPrintStreamクラスを使うことにより実現します。 |
4 | コントローラが基底クラスのexecuteを呼び出し、基底クラスのexecuteのtryブロックの中で、派生クラスのexecuteが呼ばれるのでしょうか?あるいはコントローラが呼び出すのは、基底クラスで定義された何か別のメソッドであって、そのメソッドのtryブロックの中で、(オーバライドされた)executeを呼び出すのでしょうか? | どちらもありえると思います。記事中の呼び出し例は前者にあたります。ここで言いたい事は「基底クラスにまとめるべき」という部分で、このクラス図はあくまでサンプルとお考えください。 |
5 | RuntimeExceptionをExtendsしていますが他の例外クラス(Error、Exception)を継承したユーザ定義例外を設計することは実際はあるのでしょうか?。使い分けなどがどうもわからず・・。 | Errorはシステムが全く動作できないような異常の際に使います。まず通常の用途では使うことはありません。Exceptionはよく使いますが、throws宣言が必要で、ソースのthrows宣言が肥大化し、最後は管理不能になる危険があります。使い分けですが、業務エラー(アプリレベルの続行可能な想定内のエラー、例えば不正な入力データ、操作エラー)はRuntimeException、低レベルAPIなどで例外の種類により処理を分けることが必要なエラー(例えば、通信失敗)はException、を継承するのが望ましいと思います。 |