argius note

プログラミング関連

ログにソースの行番号を出力させるには

id:sardine:20070525#p1
C言語などでは、マクロによって行番号をprintしたりできるわけですが、Javaでは単純な手段でこれを実現することはできません。ところが、Log4jのレイアウトにはフォーマットには、行番号を表示するパターン(%L)がサポートされています。Log4jは、PureJavaな製品なのでJNIを使っているわけでもないのに、何故そんなことが可能なのでしょうか。
答はid:sardineさんのところで解説されていますので、私が実際に使わせていただいているコードを紹介することにします。ちなみに、Log4jのソース自体はまだ見ていません。本家はもっと洗練されたコードなのかも知れませんが、敢えて見ていないことをご承知ください。

static int getLineNumber(String targetClassName) {
    try {
        throw new Exception();
    } catch (Exception ex) {
        StackTraceElement[] elements = ex.getStackTrace();
        for (int i = 0; i < elements.length; i++) {
            StackTraceElement element = elements[i];
            if (element != null // 2007.06.14 修正
                && targetClassName.equals(element.getClassName())) {
                return element.getLineNumber();
            }
        }
    }
    return -1;
}

コンパイル時にデバッグ情報の行番号を生成しないオプションを与えている場合は、StackTraceElement#getLineNumber()は「負の数値*1」を返します。仕様では値を特定していませんが、SUNのJDKでは"-1"が返ってくるようです。このメソッドもそれに倣っています。
StackTraceElementはJava1.4からサポート(例外チェーン機能)されていますので、Java1.3以前なら「Throwable.printStackTrace()をStringWriterなどで取り出して文字列として解析*2」すれば実現できるようです。
Log4jを使えば済むことなのですが、hackのひとつの例として捉えるのが吉でしょう。

*1:Javadocの日本語版に書かれている。

*2:Log4jはJava1.2,1.3もサポートしているので、こちらの方法を使っているらしい?

Apache Velocity

汎用テンプレートエンジン。近々、ソースコードジェネレータを作ることになりそうなので調べておこうかな、と思いました。割と前から気になってはいたんですが、なかなか手をつけられずにいたんですよ。
どの辺が汎用かっていうと、データを決める部分はjava.util.Mapに似た構造のクラスで、合成結果の出力先はjava.io.Writerで、テンプレートはスクリプト風の独自テンプレート言語(VTL)で記述するところです。最後のは汎用じゃないように見えますが、独自言語と言っても、既存のスクリプト系言語をベースにしたようなものなので、余程複雑なことをやるのでなければ一瞬で理解できるでしょう。

では練習をしてみます。テキストファイル"test.vm"に次のテンプレートを記述します。

#set($v = "123")
name = $name
$v
#if ($number == 3) three! #else not three. #end

 #foreach ($i in $list) element = $i #end

実行するJavaのコードはこんな感じで。(このブロック全体は"throws Exception"が必要です。)

Velocity.init();
VelocityContext context = new VelocityContext();
context.put("name", "argius");
context.put("number", new Integer(3));
context.put("list", Arrays.asList(new Object[]{"@", "*"}));
Template template = Velocity.getTemplate("test.vm");
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
template.merge(context, out);
out.flush();

"VelocityContext"は、インタフェースとしてはMapのような感じで使えば良いようです。コンストラクタにMapのインスタンスを渡せば、Mapからの変換もできます。"Writer"には、PrintWriterやFileWriterなどの標準クラスが使用できます。最後に、"Template"に、"context"と"Writer"を渡せば合成完了となります。
実行結果です。ここでは標準出力に結果を印字しています。ここでは、PrintWriter#flush()を実行しないと出力されませんので注意。

name = argius
123
 three! 
  element = @  element = * 

はい、できました。物凄くとっつき易い仕様ですね。例えば、WriterにServletResponse#getWriter()を使えば、サーブレットJSPの代わりに使えたりします。実際、Velocityを採用しているフレームワークも多いようですね。