#はじめに
自分自身、結構適当と言うか、
エラーハンドリング(例外処理)とログ出力(メッセージ)の設計を、
なんとな〜くやってたので、しっかりと調べてみやした。
本当だったら、
どこでどんなエラーが起こって、どうやったら解決できるか
っていう、
エンジニアにヒントと希望を与えてくれるものなのに、
タスクが盛り盛りで、作ることだけに集中してしまうあまりに、
運用と管理保守のことを軽視してしまっておりました。
なんとも、Viciousスパイラル(悪循環)で、
後々自分が苦しまないように、しっかりと準備したいと思います。
(※もろもろ間違っておりましたらごめんなさい。勉強し直します。)
#1. エラー分類
大まかに分けるとエラーの種類は2つある。
###正常系エラー
例えば、トランザクション開始前のバリデーションチェックなど、
想定内として、入力エラー等を正常処理としてエラーハンドリングするケース
####正常系エラーは、例外は使わずに、ステータスコードを使うべし
なぜなら、例外処理は対応コストがかかるため、例外をthrowしない。
また、入力エラーは複数の発生原因が考えられるから、「例外は1つ返す」の原則から離れてしまう
加えて、UIが関連してくるので、ステータスコードを使った方がいい。(例外としては扱わない)
例:「ステータスは、0:未完了/1:完了で入力して下さい。」など
###異常系エラー(例外)
例えば、他の機能とシステム連携中に、フォーマットチェックにひっかかったなど、
想定外に、トランザクションの続行が不可能等、そういった異常系の例外が発生したケース
開発者が任意で例外を生成しthrowするケースもあります。
以下、異常系エラーハンドリングについて触れて参る。
#2. 例外クラスを定義する
ユーザー定義例外を使うことで、例外処理を統一した形式で実行できるのでオススメ
[eclipse](https://www.eclipse.org/downloads/)
とか、
いわゆる統合開発環境を使うと、
継承元の親クラス(super)の初期化や、定義したクラスのコンストラクタ生成なんかを自動でやってくれるので便利。
今回は、**非チェック例外であるRuntimeException
**を継承したい。
普通RuntimeException
系の例外は
**「これ以上処理できないような致命的な異常が発生した」**という状態なので、
JVMや、フレームワーク等に処理方法を任せるのがセオリーな姿。
ちなみに、Exception
を継承すると、チェック例外となり、必ずtry~catch~
構文が必要になり、必ずcatch節で処理しなければいけない模様。
public class MyRuntimeException extends RuntimeException {
private static final long serialVersionUID = 1L;//シリアライズ可能なクラスとしてのマーク
protected String msgCode;
protected int level;
public MyRuntimeException() {
super();
}
public MyRuntimeException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public MyRuntimeException(String message, Throwable cause) {
super(message, cause);
}
public MyRuntimeException(String message) {
super(message);
}
public MyRuntimeException(Throwable cause) {
super(cause);
}
//これを throw newで使いたい
public MyRuntimeException(String message, String msgCode, int level) {
super(message);
this.msgCode = msgCode;
this.level = level;
}
public String getMsgCode() {
return msgCode;
}
public void setMsgCode(String msgCode) {
this.msgCode = msgCode;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
}
###定義のポイント
-
Java API
で定義されている例外、つまり、デフォルトで提供されている標準機能としては、「メッセージ」「スタックトレース」の属性しか持たない。 - 運用監視には**「メッセージコード」「ログレベル」**も必要なので、ユーザー定義クラスを新規作成。
- 属性を追加したい場合は、ユーザー定義例外のサブクラスを作るとよき◎
-
RuntimeException
を継承したクラスは、メソッドのthrowsを記述する必要がなくなり、ソースがスッキリする
public void someMethod() throws RuntimeException {
}
//↓つまりこんな感じで省略可能ってことか
public void someMethod() {
}
#3. 例外処理を基底クラス(親)にまとめる
例外処理の構文try~catch
は、処理の実行を呼び出す**「根っこ」の部分のみに実装**するとよいとのことなので、
早速以下、基底クラスのイメージです。
//基底クラスのざっくりイメージ
public class SuperBusinessService {
public static void someCommonMethod() {
try {
System.out.println("処理を始めますよ。");
//例外がスローされる可能性がある汎用処理
//e.g. DB接続や、共通として必要データのロードなどの汎用処理など。
if(A == "error"){
generateMyException1();
} else {
generateMyException2();
}
} catch(MyRuntimeException e) {
//例外1をキャッチしたときの処理;
System.out.println(e.getMsgCode());//独自エラーコードを表示
System.out.println(e.getLevel()); //独自エラーメッセージを表示
e.printStackTrace();//スタックトレース(処理の呼出履歴のことで、エラー発生までに、どのメソッドをどこで呼び出したかを表示)
// ※catch節は、必要に応じて、例外の数だけ指定可能
// } catch(例外2の型 変数) {
// //例外2をキャッチしたときの処理;
}
finally {
//必ず実行される処理;
System.out.println("処理が終わりましたよ。");
}
}
public static void generateMyException1() {
throw new MyRuntimeException("エラーを強制的に発生させました。", "E0001", 1);//e.g. 1: error (logLevel)
}
public static void generateMyException2() {
throw new MyRuntimeException("エラーを強制的に発生させました。", "E0002", 2);//e.g. 2: warn (logLevel)
}
}
派生クラス(サブ、子)のイメージ。
基底クラスを継承し、汎用処理などのメソッドがそのまま使えるようになり、
子クラスで、try~catch
やthrow
などしなくても良くなった。
こうやって処理をまとめていけるってことか。
//派生クラスのざっくりイメージ
public class SubBusinessService extends SuperBusinessService {
//内容
}
###定義のポイント
- 業務ロジックや汎用処理で発生する例外を、まとめて基底クラスで処理するように設計。
- まとめることで、各業務ロジック内で、個別に例外処理を実装する必要はなくなる。
- 例外処理が基底クラス(親)にまとまることで、ロギング(ログ出力)全集中できる。
- catch句には業務処理を書くべきではない。あくまで、例外の発生と伝えてくれる熱いメッセージ。
- 例外発生したときの対応コストは想像以上に高いので、正常処理系を例外に組み込むべきではない。
- 例外は一つの発生原因を返すように、簡素化すべき。
#4. ログ出力のポイント
上記の例でも少し、取り入れたように、
ロギングも基底クラスにまとまるので、
業務ロジック内で開始メッセージなどをロギングする必要がない。
メッセージの種類と、ログ出力の箇所をまとめた表。
種類 | 箇所 |
---|---|
業務ロジックの開始メッセージ | try句の先頭 |
業務ロジック内で発生した例外のメッセージ | catch句 |
業務ロジックの終了メッセージ | finally句 |
実際のコードに置き換えると、いかのように。
※underlyingとは「基礎となる」「元となる」「根本的な」みたいな意味です。
public class UnderlyingSuperClass {
try {
//成功処理
System.out.println("ロジックの開始");
} catch (Exception e) {
//例外処理
e.printStackTrace();
} finally {
//いずれの場合でも最後にする処理
System.out.println("ロジックの終了");
}
}
###ログに出力すべき内容
マルチスレッド・複数ユーザーという特徴をもつWebアプリケーションでは、
スレッド番号とユーザーIDは必ず出力するのがポイント。
これがないと、ユーザーごとの処理をログで追うことができないから。
以下、ロギング項目をまとめました。
基本的に、1つのログファイルに出力していきます。
※スタックトレースは標準エラー出力で対応
項目 | 説明 |
---|---|
時刻 |
「yyyy/MM/dd hh:MM:ss.SSS」形式で、西暦からミリ秒まで出力。 ※実行時間を知りたい場合、ミリ秒は必須。 |
スレッド番号 | スレッド固有の番号。マルチスレッドアプリケーションで必須 |
ユーザーID | システム利用者番号。複数ユーザーがいるアプリケーションで必須 |
ログレベル | 実装・運用フェーズでログの出力量を制限するために設定するもの。 ※ Log4j の場合:fatal, error, warn, info, debug, trace を指定 |
メッセージコード | ログ一行毎の分類コード、もしくはエラーコード。 ※例:E0001など |
任意メッセージ | エラーメッセージなど、日本語の説明。 ※エンジニアに希望を与える |
スタックトレース | 例外の発生箇所までの呼出階層。 |
##Exceptionクラスの種類一覧
Javaでよく使われるExceptionクラスの一覧です。
例外クラス | 説明 | 例 |
---|---|---|
java.lang.IllegalArgumentException | 不適切な引数 | 引数の指定エラー |
java.lang.IllegalStateException | 不正な状態 | 引数の指定エラー |
java.lang.NullPointerException | Nullアクセス | 値がNullのケース |
java.lang.IndexOutOfBoundsException | 範囲外 | 配列のindex番号が無い |
java.lang.ArithmeticException | 不正な算術計算 | ゼロでの割り算 |
java.lang.NumberFormatException | 不正な数値型への変換 | 変換元の文字列が数値じゃない |
java.io.FileNotFoundException | ファイルが開けない | ファイルが存在しない |
java.io.IOException | 不正な入出力 | readLine()を呼び出す前に標準入力(System.in)を閉じてしまうとか |
##HTTPエラーコード種類一覧
###500番台(サーバーエラー)
原因:サーバーがリクエスト処理で失敗したときに返すコード
エラーハンドリングとも関連が出てくるので覚えておくとよき。
番号 | 項目 | 説明 |
---|---|---|
500 | Internal Server Error | サーバ内部でエラーが発生した場合に返す。 サーバサイドのプログラムの文法エラーや、誤った設定があった場合などに返す。 |
501 | Not Implemented | WebDAVが実装されてないサーバに対して、固有のメソッド(例:MOVEやCOPY)を使用したときなどに返す。 |
502 | Bad Gateway | ゲートウェイ・プロキシサーバで不正な要求を受け取って、拒否した場合に返す。 |
503 | Service Unavailable | 過負荷やメンテナンス中で、サービスが一時的に使用不可であるときに返す。アクセス集中でサーバ処理が追いつかない場合など。 |
504 | Gateway Timeout | ゲートウェイ・プロキシサーバが制限時間内に処理できなかった場合に返す |
505 | HTTP Version Not Supported | リクエストがサポートされてないHTTPバージョンであるときに返す。 |
506 | Variant Also Negotiates |
Transparent Content Negotiation in HTTP で定義されている拡張ステータスコード。ネゴシエートを試みてエラーになった場合に返す。 |
507 | Insufficient Storage | WebDAVの拡張ステータスコード。リクエスト処理のために必要なストレージ容量が不足したときに返す。 |
508 | Loop Detected | サーバ内でリダイレクトループに入ったときに返す。 ※開発中にはたまに見かける。 |
509 | Bandwidth Limit Exceeded | サーバの帯域幅(転送量)を使い切ったときに返す。 |
510 | Not Extended |
An HTTP Extension Framework で定義されている拡張ステータスコード。拡張不可のときに返す。 |
###400番台(クライアントエラー)
ちなみに、HTTPのエラーコードなんかもここに載せておきます。利便性と自己啓発のため。
サーバーサイドの例外処理なので、関連しないものもありますが。。
原因:クライアントからのリクエストが成功しなかったときに返すコード
番号 | 項目 | 説明 |
---|---|---|
400 | Bad Request | 存在しないメソッドを使用したり、クライアントのリクエスト異常のときに返す。 |
401 | Unauthorized | Basic認証やDigest認証などで使われるやつ |
402 | Payment Required | 支払いが必要であるとき使われるやつ。 ※将来のために予約されているやーつ。現在は未実装らしい。 |
403 | Forbidden | リソースへのアクセス拒否。 例:サーバ構築時に権限設定をミスったときとか |
404 | Not Found | リソースが見つからなかったときのやつ |
405 | Method Not Allowed | 無許可のメソッドを使用しようとした時のやつ。 特に、POSTメソッドの使用が許されていないURIにPOSTでアクセスしたケースなど |
406 | Not Acceptable | Accept関連のヘッダに受理できない内容が含まれている場合に返されるやつ。Accept-Language、Accept-Charset などでファイル形式で、ブラウザが言語許可していないときのやつ。 |
407 | Proxy Authentication Required | プロキシ認証が必要な場合に返されるやつ。 |
408 | Request Timeout | リクエストが時間内に完了しない場合に返されるやつ |
409 | Conflict | リクエストが、いま現在のリソースと矛盾していて完了できないケース |
410 | Gone | 「リソースは恒久的に移動・消滅した。どこに行ったかもわからない。」みたいな特殊ケース。404 Not Found と似ているけど、Goneは二度と復活しないケースに使われるらしい |
411 | Length Required |
Content-Length ヘッダがなくて、サーバがアクセス拒否したときのやつ |
412 | Precondition Failed | 前提条件が誤りだった場合に返されるやつ。 |
413 | Request Entity Too Large | リクエストエンティティがサーバの許容範囲を超えてしまっていた場合に返すやつ。 例:サーバ制限のサイズより大きいファイルをアップロードなど。 |
414 | Request-URI Too Long | URIが長過ぎて、サーバが処理を拒否ったときのやつ。 例:動画データなどをGETで送ったようなケース。 |
415 | Unsupported Media Type | サーバで指定メディアタイプがサポートされてないときのやつ。 |
416 | Requested Range Not Satisfiable | 実際のリソースサイズを超えるデータを要求したときのやつ。 例:リソースサイズが1024Byteしかないのに、1025Byteを取得しようとした |
417 | Expectation Failed | サーバが拡張ステータスコードを扱えない場合に返す。 |
418 | I’m a teapot |
HTCPCP/1.0 の拡張ステータスコード。ティーポットにコーヒーを淹れるリクエストが来て、拒否された場合に返す。 |
422 | Unprocessable Entity | WebDAVの拡張ステータスコード。 |
423 | Locked | WebDAVの拡張ステータスコード。リクエストしたリソースがロックされている場合に返すやつ。 |
424 | Failed Dependency | WebDAVの拡張ステータスコード。依存関係でエラーの場合に返すやつ |
426 | Upgrade Required |
Upgrading to TLS Within HTTP/1.1 の拡張ステータスコード。アップグレード必須なときに返すやつ |
とまあ、知らんものも多いし、どれかは今後一切使わないだろうな〜
##その他HTTPステータスコード
ここで、詳しくは触れませんが、ざーーーーっくりと、書きますw
100番台: Informational
200番台: Success
300番台: Redirection
ほお。なるへそ。
まとめるの疲れたお
#まとめ
例外処理とロギングの設計は、
美しさと言うよりかは、
現実的な設計、つまり汎用的かつシンプルなできるだけ手間のかからない設計がやっぱり大事。
ここでどんな例外が起きたのか、シンプルに原因だけをわかりやすいメッセージとコードで伝える。
システムが「ああ」と言ったらエンジニアが「こう」と叫べるように、
阿吽の呼吸が大事ですね。
###一番やっちゃいけないのは、
- ロジックの中に
try ~ catch
を散りばめる - 例外処理が統一されていない、各々のやり方でやってしまっている。
- ログに例外を出力してない。なんの例外なんだ・・・状態
- 誤ったフォーマットで出力する
- 一度の例外で複数回スタックトレースを出力する。グチャッててわけわかめ。
- すべてのクラスに開始・終了メッセージのログ出力がなされてしまい、大量に出力されたログに翻弄される。かつ、パフォーマンスが非常に悪い。
- ログにユーザーIDがないことで、誰の操作でエラーが発生したのかわからない。犯人探しができない。
一番っていってるのに、いっぱい書いちゃいましたねw
例外処理とログ出力の出来次第で、
運用開始後の障害の収束スピードと安定性が変わっていく。。
####つまり、サービスの信用と信頼に繋がっていく。
以上、
ありがとうございました!!