System.out を置き換えよう

Java では System.out.println メソッドhello world 等のメッセージを標準出力に出力できるが、この出力先は変更可能なのです。

System クラスには setOut メソッドがあり、このメソッドを用いて System.out を置き換える事ができる。
デフォルトでは標準出力になっているが、setOut でファイルに出力する PrintStream を渡せば以降 System.out.println や System.out.print メソッドの引数がファイルに出力される。
コード例

public class ReplaceStdOutTest {
    public static void main(String[] args) {
        try {
            // debug.log に出力する PrintStream を生成
            PrintStream out = new PrintStream("C:\\debug.log");
            
            //置き換える
            System.setOut(out);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        // この hello world! は debug.log に出力される。
        // (コンソールには何も出力されない)
        System.out.println("hello world!");
    }
}

上記コードで C:\debug.log に "hello world!" が書き込まれる。
このテクニックを使えば、System.out.println の出力先を開発時のモードで変える事が出来る。

デベロップ時 -> 標準出力
デバッグ時 -> ファイル
リリース時 -> どこにも出力しない

標準出力だけでなく、標準エラー出力、標準入力も置き換える事が出来る。
また、標準出力に加えてファイルにも出力するということも可能だ。


ログファイルには日付は必須、と考える多くの人は以下のように PrintStream を継承したクラスを作って対応する。
出力時に日付を出力する MyPrintStream の例

class MyPrintStream extends PrintStream
{
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss] ");
    public MyPrintStream(String fileName) throws FileNotFoundException {
        super(fileName);
    }

    // 現在日時をフォーマットして文字列で返す
    private String getDateTime(){
        return FORMATTER.format( new Date() );
    }

    // それぞれ、引数に getDateTime() を加えることで全出力に日時が記録される。
    @Override public void print(boolean b) { super.print( getDateTime()+b); }
    @Override public void print(char c) { super.print( getDateTime()+c); }
    @Override public void print(char[] s) { super.print( getDateTime()+String.valueOf(s)); }
    @Override public void print(double d) { super.print(getDateTime()+d); }
    @Override public void print(float f) { super.print(getDateTime()+f); }
    @Override public void print(int i) { super.print(getDateTime()+i); }
    @Override public void print(long l) { super.print(getDateTime()+l); }
    @Override public void print(String s) { super.print(getDateTime()+s); }
    @Override public void print(Object obj) { super.print(getDateTime() + String.valueOf(obj)); }
}

使い方はこう。

public class ReplaceStdOutTest {
    public static void main(String[] args) throws FileNotFoundException {
        try {
            // 上記 MyPrintStream を生成して setOut.
            System.setOut( new MyPrintStream("C:\\debug.log") );
        } catch (Exception e) {
            ...
        }

        // 下記の処理ではコンソールではなく debug.log に出力される。
        System.out.println(true);
        System.out.println(123);
        System.out.println("test");
    }
}

C:\debug.log は以下のように出力される。

[2011-09-17 15:21:19] true
[2011-09-17 15:21:19] 123
[2011-09-17 15:21:19] test

うむ、グッド。
あとは少々手を加えて、リリース版なのかデベロップ版なのか判定して適宜 PrintStream を置き換えるようなコードを書き、
設定ファイルで出力先を決められたり日付ごとに出力ファイルを変えたりすればかなり実用的になると思われる。

さらに、標準エラー出力で出力する際に必要な情報を収集して追加した上で出力する、という事もできる。例えば例外発生時のメモリ使用状況やIO状況、ネットワーク稼働状況やCPU使用率JVMの設定、スレッドの数や Load Average、関連プロセスの状態(MySQLとか)、etc...。とにかく想定外のエラー発生時に収集できる情報はできるだけ収集し、どこかに記録しておくべきだ。自前の PrintStream クラスは少々複雑になると思われるが、エラー時の状況の手がかりを残せる事を考えれば大した手間ではない。

なにより、printf デバッグ( この場合は println デバッグか)時に書いたコードをうっかり消し忘れてしまい、本番環境で恥ずかしい文字が出るなんていうことも防げる。一般的には本番用のコードには System.out.println を書かず、ログは log4j 等に任せる事が多い。しかし、println を消すなんて作業は不必要で、むしろあらゆる箇所で有力な情報を出力すべきだ。


以上、System.out の置き換え方と有効な使い方でした。
それでは、良いデバッグライフを!

詳解 Javaプログラミング第2版〈VOLUME1〉

詳解 Javaプログラミング第2版〈VOLUME1〉