前々から気になっていたのと、少し前に購入したこちらの本にも紹介されていたので、JBoss ProjectsのBytemanを試してみることにしました。
JBoss Enterprise Application Platform6 構築・運用パーフェクトガイド
- 作者: NTTオープンソースソフトウェアセンタ,レッドハット株式会社
- 出版社/メーカー: 技術評論社
- 発売日: 2013/06/22
- メディア: 大型本
- この商品を含むブログ (10件) を見る
そもそもBytemanってなんですか?ってところですが、javaagentを使用してバイトコードの変更を行うツールです。これを利用して、アプリケーションの動作を変更したりできます。
Byteman
https://www.jboss.org/byteman
使い方としては、
の2つがあり、どのような処理を入れるかは、Bytemanが提供するルールに沿って記述する必要があります。
インストール
まずはこちらからダウンロード。
https://www.jboss.org/byteman/downloads
現時点での最新バージョンは、2.1.3です。
とりあえず、展開します。
$ unzip byteman-download-2.1.3-bin.zip
最低限は、ここまで。なお、スクリプトでアタッチをかける場合には、インストールディレクトリに対して環境変数BYTEMAN_HOMEを設定しておく必要があります。
$ export BYTEMAN_HOME=/path/to/byteman-download-2.1.3
ルールを書く
バイトコード操作を行うためには、ルールを書く必要があります。おおまかには、
RULE [ルール名] CLASS(またはINTERFACE) [クラス名またはインターフェース名] METHOD [メソッド名] [適用タイミング] IF [ルールを実行する条件] DO [実行されるアクション] ENDRULE
端折ってるところもありますが、大まかにはこんな感じです。1ファイル中に、複数のルールを記述することもできます。コメントは、「#」で始めて書きます。
ルールファイルの拡張子は、「.btm」とするようです。
最終的には、このあたりのドキュメントにいきつくことになると思います…。
https://community.jboss.org/wiki/ABytemanTutorial#top
http://downloads.jboss.org/byteman/2.1.3/ProgrammersGuideSinglePage.2.1.3.1.html
Javaプログラムにルールを適用する
2つの適用方法を、それぞれ紹介します。
Javaアプリケーションの起動時に適用する
javaコマンドのオプションに、javaagentとかを設定します。
$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:[ルールファイル] [メインクラス名]
もしくは
$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:[ルールファイル],boot:$BYTEMAN_HOME/lib/byteman.jar -Dorg.jboss.byteman.transform.all [メインクラス名]
下の場合は、Javaが提供するクラスにアタッチする場合に使います。
なお、この方法で実行する場合は、環境変数BYTEMAN_HOMEは必須ではありません。
起動中のアプリケーションにスクリプトで適用する
こちらでは、環境変数BYTEMAN_HOMEの設定は必須となります。
まずは、アタッチするJavaアプリケーションのPIDを特定します。
$ jps
特定したら、そのPIDに対してbminstall.shを実行します。
$ $BYTEMAN_HOME/bin/bminstall.sh [PID]
ルールファイルの適用には、bmsubmit.shを「-l」オプションで使用します。
$ $BYTEMAN_HOME/bin/bmsubmit.sh -l [ルールファイル] install rule [ルール名]
ルールをアンロードするには、bmsubmit.shを「-u」オプションで使用します。
$ $BYTEMAN_HOME/bin/bmsubmit.sh -u [ルールファイル名] uninstall RULE [ルール名]
「-b」オプションでbootstrap classpathに加えることができ、「-Dname=value」でシステムプロパティを設定できるようです。
.batファイルもあるので、Windowsでも使えそうな感じ?
使ってみる
では、実際に使ってみましょう。スケープゴートとなっていただくために、このようなクラスを用意しました。
// sample/MainClass.java package sample; public class MainClass { public static void main(String[] args) { while (true) { execute(); } } private static void execute() { SimpleImpl si = new SimpleImpl(); si.echo(); si.echo("Message From Main"); System.out.println("count = " + si.getCount()); System.out.println("totalCount = " + si.getTotalCount()); System.out.println("myMethod = " + si.myMethod("Call MyMethod")); try { si.throwException(); } catch (Exception e) { } try { Thread.sleep(3 * 1000L); } catch (InterruptedException e) { } } } // sample/SimpleInterface.java package sample; public interface SimpleInterface { void echo(); void echo(String message); } // sample/SimpleImpl.java package sample; public class SimpleImpl implements SimpleInterface { private static int totalCount = 0; private int count = 0; public String myMethod(String message) { return "***" + message + "***"; } public void echo() { System.out.println("Default Message"); count++; totalCount++; } public void echo(String message) { System.out.println("Echo = " + message); count++; totalCount++; int localCount = count; int localCount2 = localCount; } public int getCount() { return count; } public static int getTotalCount() { return totalCount; } public void throwException() { throw new RuntimeException("Oops!!"); } }
ずっと起動したまま、無限ループし続けるプログラムです。しれっとインターフェースも入れています。
では、これに実際にルールを適用してみます。
いきなりルール2つですが、こんなのを書いてみます。
sample.btm
# コメントは、「#」で記述 RULE trace entry getCount CLASS sample.SimpleImpl METHOD getCount AT ENTRY IF TRUE DO traceln("========== called getCount =========="), traceStack() ENDRULE # ルール名を分ければ、複数ルールは記述可能 RULE trace entry getTotalCount CLASS sample.SimpleImpl METHOD getTotalCount AT EXIT IF TRUE DO traceln("========== called getTotalCount =========="), traceStack() ENDRULE
それぞれ、SimpleSimple#getCountの呼び出し時とgetTotalCountの終了時にprintlnして、スタックトレースを出力します。
普通に実行すると
$ java sample.MainClass Default Message Echo = Message From Main count = 2 totalCount = 2 myMethod = ***Call MyMethod*** Default Message Echo = Message From Main count = 2 totalCount = 4 myMethod = ***Call MyMethod***
みたいな結果になりますが、Bytemanを使ってこのルールを適用すると
$ java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:sample.btboot:$BYTEMAN_HOME/lib/byteman.jar -Dorg.jboss.byteman.transform.all sample.MainClass Default Message Echo = Message From Main ========== called getCount ========== Stack trace for thread main sample.SimpleImpl.getCount(SimpleImpl.java:-1) sample.MainClass.execute(MainClass.java:14) sample.MainClass.main(MainClass.java:6) count = 2 ========== called getTotalCount ========== Stack trace for thread main sample.SimpleImpl.getTotalCount(SimpleImpl.java:32) sample.MainClass.execute(MainClass.java:15) sample.MainClass.main(MainClass.java:6) totalCount = 2 myMethod = ***Call MyMethod*** Default Message Echo = Message From Main ========== called getCount ========== Stack trace for thread main sample.SimpleImpl.getCount(SimpleImpl.java:-1) sample.MainClass.execute(MainClass.java:14) sample.MainClass.main(MainClass.java:6) count = 2 ========== called getTotalCount ========== Stack trace for thread main sample.SimpleImpl.getTotalCount(SimpleImpl.java:32) sample.MainClass.execute(MainClass.java:15) sample.MainClass.main(MainClass.java:6) totalCount = 4 myMethod = ***Call MyMethod***
のように、コンソールにメッセージとスタックトレースの出力が追加されます。
もちろん、
$ java sample.MainClass
で実行した後に
## PID特定 $ jps 22913 MainClass 22928 Jps ## アタッチ $ $BYTEMAN_HOME/bin/bminstall.sh 22913 ## ルールインストール $ $BYTEMAN_HOME/bin/bmsubmit.sh -l sample.btm install rule trace entry getCount install rule trace entry getTotalCount
でもいいですからね。
では、いくつかルールのサンプルを記載して終わりにしますね。
オーバーロードを含む特定のメソッドに対して、ルールを適用する
メソッド名の後に()を付けないと、同じ名前のメソッドに対してルールが適用されます。
RULE trace all echo CLASS sample.SimpleImpl # ()を省略した場合は、引数有り無しのすべてのメソッドに適用される # ちなみに、コンストラクタに対して適用したい場合は「<init> 」で指定 METHOD echo AT ENTRY IF TRUE DO traceln("========== call echo method START ==========") ENDRULE
メソッドの引数を指定して、ルールを適用する
メソッド名の後に()を付けて、その中に引数の型を指定(もしくは書かずに引数なしのメソッドを指定)します。
RULE trace param echo CLASS sample.SimpleImpl METHOD echo(String) # $0は自分自身(this)、$1以降でメソッド呼び出しパラメータ BIND this = $0, param = $1 AT ENTRY IF TRUE DO traceln("========== this[" + this.getClass().getName() + "] call echo method[" + param + "] START ==========") # BINDせずに、いきなり$0や$1などを使ってもOK # DO traceln("========== this[" + $0.getClass().getName() + "] call echo method[" + $1 + "] START ==========") ENDRULE
ここでは、BINDを使った例を書いています。$0がthisで、$1から始まる$Nはメソッドの引数を表します。もちろん、BIND自体はメソッドの引数を使う場合に限ったことではありません。
メソッドの戻り値を取得する
$!でメソッドの戻り値をキャプチャすることができます。ちなみに、「RETURN」はEXITのシノニムらしいです。まあ、いずれもメソッドの終了時ですね。
RULE trace method return CLASS sample.SimpleImpl METHOD getCount() AT RETURN IF TRUE # $!でメソッドの戻り値 DO traceln("========== getCount Return Value[" + $! + "] ==========") ENDRULE
インターフェースに対して、ルールを適用する
CLASSキーワードではなくて、INTERFACEキーワードでインターフェース名を指定することで、インターフェースの実装クラスに対してルールを適用することができます。
RULE trace interface INTERFACE sample.SimpleInterface METHOD echo() AT ENTRY IF TRUE DO traceln("========== call Interface echo method START ==========") ENDRULE
メソッドから例外がスローされた時に、ルールを適用する
AT THROWで例外スロー時、$^でスローされた例外を使うことができます。
RULE trace throw exception CLASS sample.SimpleImpl METHOD throwException AT THROW IF TRUE # $^で投げられた例外 DO traceln("========== in throwException() e = [" + $^ + "] =========="), traceStack() ENDRULE
今回の例では、例外を握りつぶしている行儀の悪いコードなので、スタックトレースを出力しています。
フィールドの読み書き
「AT READ」や「AFTER READ」でフィールドを読む時や読んだ後、
RULE trace field read CLASS sample.SimpleImpl METHOD echo AT READ count IF TRUE DO traceln("========== in echo() read count[" + $0.count + "] ==========") ENDRULE
「AT WRITE」や「AFTER WRITE」でフィールドに書き込む時や、書いた後
RULE trace field write CLASS sample.SimpleImpl METHOD echo AFTER WRITE count IF TRUE DO traceln("========== in echo() writed count[" + $0.count + "] ==========") ENDRULE
それぞれに対して、ルールを適用できます。
staticフィールドに対して適用する
実は、フィールドと一緒です。
RULE trace static field read CLASS sample.SimpleImpl METHOD echo AFTER WRITE totalCount IF TRUE DO traceln("========== in all echo totalCount writed = [" + sample.SimpleImpl.totalCount + "] ==========") ENDRULE
ローカル変数に対して、適用する
条件付きで、「AT READ」や「AFTER WRITE」などをローカル変数に対しても適用することができます。
条件は、コンパイル時に「-g」オプションを指定するか、
$ javac -g [ソースファイル]
もしくは、「-g」オプションに少なくとも「vars」を加えておく必要があります。
$ javac -g:vars [ソースファイル]
ルールのサンプル。
# ローカル変数を使うには、 #「javac -g」もしくは「javac -g:vars」を含めてコンパイルしておく必要がある RULE trace local var write CLASS sample.SimpleImpl METHOD echo(String) # $ローカル変数名で参照可能 AFTER READ $localCount IF TRUE # $ローカル変数名で参照可能 DO traceln("========== in echo(String) writed localVar[" + $localCount + "] ==========") ENDRULE
ローカル変数は、「$ローカル変数名」で参照することができます。
実際使ってみて、やっぱりなかなか面白かったです。機会を見つけて、活用していこうと思います。