黒魔術Byteman使ってみた
過去のエントリでも紹介したBytemanを実際に触ってみたのでとりまとめ。
参考サイト
公式:https://www.jboss.org/byteman
ダウンロード:https://www.jboss.org/byteman/downloads
バイトコード操作ツール、Bytemanを試す:http://d.hatena.ne.jp/Kazuhira/20131022/1382455739
Bytemanクイックリファレンス:http://d.hatena.ne.jp/nekop/20111226
この辺を読めば使い方はおおよそわかってくると思うので、実際のコードに入れてく使い方をつらつらと書いていきたいと思います。
前準備
まずは、Bytemanの黒魔術の被害者となるコードを作成。まあ日付をフォーマットして標準出力するだけです。
Eclipseなら「実行の構成」のJVMパラメータにByteman用の設定オプションを追加しておけばBytemanが使えます。
import java.text.SimpleDateFormat; public class BytemanTest { public static void main(String[] args) { try{ System.out.println("■Bytemanの実行テスト開始"); //システム日付の呼出、日付の文字列(yyyyMMdd)変換 String yyyymmdd; SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); yyyymmdd = dateFormatter.format(new java.util.Date()); //例外注入のテスト用 System.out.println("■Integer.parseInt(\"0\"): " + Integer.parseInt("0")); //結果出力 System.out.println("■main内での結果: "+ yyyymmdd); System.out.println("■getYYYYMMDD()の結果: " + getYYYYMMDD()); System.out.println("■Bytemanの実行テスト終了"); }catch(Exception e){ System.out.println("■例外発生"); } } private static String getYYYYMMDD(){ //システム日付の呼出、日付の文字列(yyyyMMdd)変換 // (mainメソッド内での実施内容と同様) SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd"); return dateFormatter.format(new java.util.Date()); } }
何も使わずに実行したら、標準出力はこうなります。
実行結果
■Bytemanの実行テスト開始 ■Integer.parseInt("0"): 0 ■main内での結果: 20140318 ■getYYYYMMDD()の結果: 20140318 ■Bytemanの実行テスト終了
指定したメソッド開始時にトレースログ出力
ここで、Bytemanスクリプトの初歩である、メソッドの開始時(AT ENTRY)やメソッドの起動時(AT INVOKE)に標準出力にトレースを出力するスクリプトを加えてみます。
Bytemanスクリプト
# BytemanTestのmainメソッド「開始時」にトレースを標準出力 RULE trace main entry CLASS BytemanTest METHOD main AT ENTRY IF true DO traceln("□Byteman: main() AT ENTRY") ENDRULE # SimpleDateFormatのformatメソッド「開始時」にトレースを標準出力 RULE trace entry simpledateformat format CLASS java.text.SimpleDateFormat METHOD format AT ENTRY IF true DO traceln("□Byteman: java.text.SimpleDateFormat.format() AT ENTRY"); ENDRULE # java.util.Dateのコンストラクタ「開始時」にトレースを標準出力 RULE trace date CLASS java.util.Date METHOD <init> AT ENTRY IF true DO traceln("□Byteman: java.util.Date() AT ENTRY"); ENDRULE # BytemanTestのmainメソッド内で # java.text.SimpleDateFormat.format(Date)が「起動時」にトレースを標準出力 RULE trace invoke simpledateformat format CLASS BytemanTest METHOD main AT INVOKE java.text.SimpleDateFormat.format BIND NOTHING IF true DO traceln("□Byteman: java.text.SimpleDateFormat.format() AT INVOKE") ENDRULE
なお、テキストファイルは文字コード UTF-8のBOMなしです。
結果がどうなるかというと、こんなかんじになります。
実行結果
□Byteman: main() AT ENTRY ■Bytemanの実行テスト開始 □Byteman: java.util.Date() AT ENTRY □Byteman: java.util.Date() AT ENTRY □Byteman: java.util.Date() AT ENTRY □Byteman: java.text.SimpleDateFormat.format() AT INVOKE □Byteman: java.text.SimpleDateFormat.format() AT ENTRY □Byteman: java.text.SimpleDateFormat.format() AT ENTRY ■Integer.parseInt("0"): 0 ■main内での結果: 20140318 ■getYYYYMMDD()の結果: 20140318 ■Bytemanの実行テスト終了
AT INVOKEとAT ENTRYのタイミングの違いについてですが、AT INVOKEは「対象ステップの処理開始時」、AT ENTRYは「メソッド内の処理に入った瞬間」でタイミングが異なるという理解でたぶんいいかなと。
スクリプトの文法についてはBytemanクイックリファレンスを見てもらうのが早いですが、CLASS句 METHOD句で対象を指定する。タイミングをAT/AFTER句で指定する。スコープ外のローカル変数などをDO句やIF句内で使いたいならBIND句で指定、IF句はDO句の実行可否を判定する場合に指定、といった形で覚えて置けばよいかと思います。
ちなみにRULE句で指定した名前がかぶるとエラーになるので注意してください。(何回かやらかしたw)
メソッドのリターンやパラメータのトレース出力
メソッドのリターン値やパラメータを取りたい場合、以下のような変数表現でバインドする事になります。
変数のスコープが有効な範囲内であれば、普通にメソッド内のローカル変数も指定できます。
ただ、中身がちゃんと入っているかはByteman側のコードの呼び出しタイミングとの兼ね合いになるので、
うまく出力できない場合はソース上の処理順序などを考えながらAT句のタイミングを変えるなどしてみてください。
パラメータやリターン値に関する変数表現はこんなのがあります。
- $1, $2, $3...
- メソッドパラメータ
- $!
- リターン値、AT EXITかAFTER INVOKEのみ有効
- $^
- スローされた例外、AT THROWで有効
- $#
- メソッドのパラメータ数
Bytemanクイックリファレンス
パラメータをトレース出力したいとか、処理結果をトレース出力したい場合なんかは上のものが使えます。
例えば、SimpleDateFormat.format()のリターンをトレースで出したいような場合はこんなかんじのスクリプトを書く事になります。
Bytemanスクリプト
# 特定メソッド(SimpleDateFormat.format()のリターンをトレース出力) RULE trace yyyymmdd CLASS BytemanTest METHOD main AFTER INVOKE java.text.SimpleDateFormat.format BIND yyyymmdd = $! IF true DO traceln("□Byteman: yyyymmdd:" + yyyymmdd.toString()) ENDRULE
実行結果
■Bytemanの実行テスト開始 □Byteman: yyyymmdd:20140318 ■Integer.parseInt("0"): 0 ■main内での結果: 20140318 ■getYYYYMMDD()の結果: 20140318 ■Bytemanの実行テスト終了
上記の例だと、AFTER INVOKE、つまりメソッドの処理が終わった段階なので、
13行目のyyyymmdd = dateFormatter.format(new java.util.Date());が処理された後となり、
yyyymmddが普通に呼び出せる状態になってるので、普通にprintlnできるわけですね(たぶん)。
タイミングによっては変数からトレース出した方がいいかもしれませんけど、
基本IF句で使うものかなーという雑感。
特定メソッドの呼び出し時に例外をスロー
エラー系の試験とかで明示的に例外を投げたいという場合もあると思います。
そんな場合は以下のようなBytemanスクリプトを作成します。
Bytemanスクリプト
# IntegerのparseIntメソッド「開始時」に例外 RULE throw Exception Injection CLASS BytemanTest METHOD main AT INVOKE java.lang.Integer.parseInt IF true DO traceln("□Byteman: 例外注入");throw new NumberFormatException() ENDRULE
parseIntが呼び出されたら、例外をスローするスクリプトですね。
ここでスローできる例外の種類ですが、対象のメソッドのthrow句に指定されてるものか、RuntimeExceptionのみです。
ありとあらゆる例外を好きなタイミングで出せるわけではないので、そこは注意です。
実行結果
■Bytemanの実行テスト開始 □Byteman: 例外注入 Exception in thread "main" java.lang.NumberFormatException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:526) at org.jboss.byteman.rule.expression.ThrowExpression.interpret(ThrowExpression.java:231) at org.jboss.byteman.rule.Action.interpret(Action.java:144) at org.jboss.byteman.rule.helper.InterpretedHelper.fire(InterpretedHelper.java:169) at org.jboss.byteman.rule.helper.InterpretedHelper.execute0(InterpretedHelper.java:137) at org.jboss.byteman.rule.helper.InterpretedHelper.execute(InterpretedHelper.java:100) at org.jboss.byteman.rule.Rule.execute(Rule.java:684) at org.jboss.byteman.rule.Rule.execute(Rule.java:653) at BytemanTest.main(BytemanTest.java:16)
特定メソッドの結果を入れ替える
特定のメソッドのリターンを入れ替える、なんていう黒い使い方もできます。
やり方としてはメソッドの起動時(AT ENTRY)にBytemanスクリプトを呼び出して、
Bytemanスクリプト側で先にreturnを返しちゃうような使い方ですね。
Bytemanスクリプト
# 特定メソッド(BytemanTest.getYYYYMMDD()のリターン入替) RULE trace exchange getYYYYMMDD CLASS BytemanTest METHOD getYYYYMMDD AT ENTRY BIND NOTHING IF true DO traceln("□Byteman: BytemanTest.getYYYYMMDD()のリターンを置き換える"); return "20130101" ENDRULE
上記のスクリプトを使うとあら不思議、getYYYYMMDD()からは固定で"20130101"が返ってくるようになります。
実行結果
■Bytemanの実行テスト開始 ■Integer.parseInt("0"): 0 ■main内での結果: 20140318 □Byteman: BytemanTest.getYYYYMMDD()のリターンを置き換える ■getYYYYMMDD()の結果: 20130101 ■Bytemanの実行テスト終了
本当はnew Date()の内容をシステム日付以外で置き換えたかったんですが、なぜかうまく行かず・・・。