この記事は、Java Advent Calendar 2015 の 14 日目の記事です。
昨日は @skrb さんの JRE をカスタマイズ - jlink でした。
明日は @yukung さんです。
最近存在を知ったので、使い方を調べてみました。
最後に +α も用意してますので、よければ見てやってください。
環境
OS
Windows 7 64bit SP1
Java
1.8.0_65
Byteman とは
読みは、たぶん「ばいとまん」。
Java プログラムの任意の場所に任意のコードを差し込むことができるライブラリ(ツール)。
予め差し込むことも、動いているプログラムに後から差し込むこともできる。
今動いているプログラムを停止させることなく中の動作を検証できるので、再起動やデバッガで動作を止めたりできない環境の検証とかに使えるかもしれない。
公式ではテストで使う方法が紹介されており、 BMUnit という JUnit で Byteman を使うためのツールが提供されているっぽい(jmockit があるからとりあえずはいいかな)。
Hello World
インストール
こちら から zip ファイルをダウンロードする。
とりあえずソースは要らないのでバイナリのみのやつをダウンロードした。
zip を解凍してできたディレクトリを BYTEMAN_HOME
という環境変数に割り当てる。
さらに、 %BYTEMAN_HOME%\bin
を環境変数 PATH
に追加する。
コマンドラインを立ち上げて、以下のコマンドを実行してインストールが完了していることを確認する。
> bmjava
usage: bmjava [-p port] [-h host] [-l rulescript | -b bootjar | -s sysjar | -nl | -nb | -nj ]* [--] javaargs
(以下略)
簡単な Java プログラムを作る
コードを差し込む対象となる、簡単な Java プログラムを作る。
public class Main {
public static void main(String... args) {
System.out.println("Hello World!!");
}
}
Hello World するだけの簡単なプログラム。
Byteman の設定ファイルを作る
差し込むコードを定義する、 Byteman の設定ファイルを作成する。
RULE hello byteman
CLASS Main
METHOD main
AT ENTRY
IF true
DO traceln("Hello Byteman!!")
ENDRULE
実行する
まずは Main.java
をコンパイルして。
> javac Main.java
Byteman の設定ファイル(hello.btm
)を指定して Main
を実行する。
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:hello.btm Main
Hello Byteman!!
Hello World!!
Hello Byteman!!
が呼びだされた!
説明
Byteman を使うときには、最低限 byteman.jar
と設定ファイル(*.btm
)が必要になる。
byteman.jar
はクラスパスで指定するのではなく、 -javaagent
オプションで指定する。
このとき、 =script:<設定ファイル名>
という形で使用する設定ファイルを指定する。
あとは、普通に Java を実行するときと同じように、メインメソッドを持つクラスを指定する。
すると、 Byteman が設定ファイルを読み込んで、設定ファイルに記述された場所にコードを埋め込んだ状態でプログラムを実行してくれる。
設定ファイルの書き方については、おいおい。
-javaagent
の記述を省略する
コマンドラインから実行するときに、毎回 -javaagent
を指定するのはしんどい。
そこで、 Byteman にはこれらの記述を省略して Java コマンドを実行できる起動用のスクリプトが用意されている。
%BYTEMAN_HOME%\bin
の下に入っている bmjava
というスクリプトがそれになる。
bmjava
を使うと、設定ファイルの記述を -l
オプションで指定できるようになる。
> bmjava -l hello.btm Main
Hello Byteman!!
Hello World!!
複数の設定ファイルを指定する
メインメソッドの後で See you Byteman!!
と出力する see-you.btm
を作成して、 hello.btm
と合わせて実行してみる。
RULE see you byteman
CLASS Main
METHOD main
AT EXIT
IF true
DO traceln("See you Byteman!!")
ENDRULE
まずは、 -javaagent
を明示する方法で。
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:hello.btm,script:see-you.btm Main
Hello Byteman!!
Hello World!!
See you Byteman!!
カンマ (,
) 区切りで script:<設定ファイル名>
の記述を続けることで複数の設定ファイルを読み込ませることができる。
次に bmjava
を使った場合。
> bmjava -l hello.btm -l see-you.btm Main
Hello Byteman!!
Hello World!!
See you Byteman!!
-l
オプションを複数宣言すればいい。
動作中のプログラムにルールを動的に追加・削除する
Byteman によるコードの差し込みは、動作中のプログラムに対しても実行できる。
ここでは、以下のプログラムを走らせて、動的にルールを追加したり削除したりしてみる。
public class Main {
public static void main(String... args) throws Exception {
int i=0;
while (true) {
print(i++);
Thread.sleep(1000);
}
}
private static void print(int i) {
System.out.println("i = " + i);
}
}
無限ループしながら、 i
をインクリメントしつつ標準出力するだけの実装。
リスナーを指定してプログラムを起動する
実行中のプログラムにルールを動的に適用するには、プログラムと一緒にリスナーを動かす必要がある。
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=listener:true Main
リスナーを一緒に起動するには、 -javaagent
の指定に listener:true
を追加する。
なお、 bmjava
コマンドを使用した場合は、デフォルトでリスナーが一緒に起動するようになっている。
現在適用されているルールを確認する
上述の方法でリスナーと一緒にプログラムを起動しておく。
その状態で別のコマンドラインを立ち上げ、以下のようにコマンドを実行する。
> bmsubmit -l
no rules installed
bmsubmit
というのが、動作中のプログラムに対していろいろするためのコマンドになっている。
現在は何もルールを指定していないので、 no rules installed
と出力される。
以下は実際に動いている時の様子。
ちなみに、 bmsubmit
コマンドは、裏では以下の Java プログラムを実行している。
> java -classpath %BYTEMAN_HOME%\lib\byteman-submit.jar org.jboss.byteman.agent.submit.Submit
ルールを追加する
以下のルールを追加してみる。
RULE see you byteman
CLASS Main
METHOD print
AT ENTRY
IF true
DO traceln("print(" + $1 + ")")
ENDRULE
print()
メソッドの実行前に差し込んで、 print(<引数の値>)
を出力するようにしている。
先ほどと同じように Main
クラスをリスナーと共に起動しておく。
そして、以下のように bmsubmit
コマンドを実行する。
> bmsubmit -l hello.btm
実際に動かしたときの様子を見ると分かりやすい。
bmsubmit -l hello.btm
を実行した瞬間、 print.btm
で記述した処理が差し込まれるようになったのがわかる(おもしろい)。
ルールを追加した状態で改めて bmsubmit -l
を実行すると、追加したルールの情報が出力されるようになっている。
> bmsubmit -l
# File print.btm line 5
RULE see you byteman
CLASS Main
METHOD print
AT ENTRY
IF true
DO traceln("print(" + $1 + ")")
ENDRULE
Transformed in:
loader: sun.misc.Launcher$AppClassLoader@14dad5dc
trigger method: Main.print(int) void
compiled successfully
ルールを削除する
次は、逆にルールを削除してみる。
削除するときのコマンドは以下。
> bmsubmit -u hello.btm
実際に動いている様子は以下。
コマンドを実行した直後に print(...)
の出力が止まったのが分かる。
bmsubmit -l
を再度実行すると、ルールが全てなくなり再び no rules installed
と出力されている。
既に起動しているプログラムにリスナーを追加する
リスナーを指定せずに起動されたプログラムに対して、後からリスナーを追加することができる。
まずは普通にプログラムを起動する。
> java Main
次に別のコマンドラインを立ち上げ、先ほど起動した Java プログラムのプロセス ID を調べる。
> jps
1584 Main
5292 Jps
プロセスID が分かったら、 bminstall
コマンドでリスナーを追加する。
> bminstall 1584
bminstall
の引数には、先ほど調べた Java プログラムのプロセス ID を指定する。
これでリスナーが登録できたので、あとは bmsubmit
でルールの追加・削除ができるようになる。
実際に動かした様子が以下。
ブートストラップクラスローダに読み込まれるクラスにコードを差し込む
java.lang.String
や java.math.BigInteger
など、ブートストラップクラスローダによって読み込まれるクラスは、そのままではコードの差し込みができない。
RULE hook toString
CLASS BigInteger
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
import java.math.BigInteger;
public class Main {
public static void main(String... args) throws Exception {
System.out.println(new BigInteger("123"));
}
}
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:toString.btm Main
Exception in thread "main" java.lang.NoClassDefFoundError: org/jboss/byteman/rule/exception/EarlyReturnException
at java.math.BigInteger.toString(Unknown Source)
at java.lang.String.valueOf(Unknown Source)
at java.io.PrintStream.println(Unknown Source)
at Main.main(Main.java:5)
エラーになる。
これは、 BigInteger
と Byteman のコードがそれぞれブートストラップクラスローダとシステムクラスローダによってロードされているために起こってるらしい。
その辺の仕組み的な話は以下で解説されています。
- java.lang.NoClassDefFoundError: org/jboss/bytem... | JBoss Developer
- (補足) クラスローダ | TECHSCORE(テックスコア)
ブートストラップクラスローダがロードするクラスにもコードを差し込みたい場合は、以下のように -javaagent
を設定する必要がある。
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:toString.btm,boot:%BYTEMAN_HOME%\lib\byteman.jar Main
toString() is called
123
boot:%BYTEMAN_HOME%\lib\byteman.jar
というのを追加する。
java.lang
以下のクラスにもコードを差し込む
これで全てのクラスにコードを差し込めるようになったかというと、そうでもない。
デフォルトでは、 java.lang
以下のクラスに対してはコードの差し込みが無視されるようになっている。
RULE hook toString
CLASS String
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
public class Main {
public static void main(String... args) throws Exception {
System.out.println("String Message".toString());
}
}
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:toString.btm,boot:%BYTEMAN_HOME%\lib\byteman.jar Main
String Message
エラーにはならないが、コードの差し込みは実行されていない。
java.lang
以下のクラスも変更できるようにするには、更にシステムプロパティを明示しなければならない。
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:toString.btm,boot:%BYTEMAN_HOME%\lib\byteman.jar -Dorg.jboss.byteman.transform.all Main
toString() is called
String Message
システムプロパティとして org.jboss.byteman.transform.all
を指定することで、 java.lang
以下のクラスにもコードを差し込めるようになる。
以上の追加の設定は、 bmjava
コマンドを使うとデフォルトで追加してくれる。
なので、以下のように設定ファイルの指定だけで java.lang
以下のクラスにもコードを差し込めるようになっている。
> bmjava -l toString.btm Main
toString() is called
String Message
ルールの記法
コメント
# ★ コメント...
RULE hook toString
CLASS String
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
-
#
で始まる行はコメント扱いになる。 -
#
より後ろがコメントになるわけではない。-
CLASS String # comment...
のように各定義の後ろにコメントっぽくつけると、良くてエラー、悪くてルールが適用されず無視されることがある。
-
ルール名
# ★↓ここ
RULE hook toString
CLASS String
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
-
RULE
で宣言する。 - そのルールの名前を定義する。
- 最低でも1つ、空白スペース以外の文字が含まれていれば、あとはどんな文字を使っても良い。
- ルールごとに一意になるようにつけたほうが良い。
クラスを指定する
RULE hook toString
# ★↓ここ
CLASS String
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
-
CLASS
で宣言する。 - パッケージ名を含めてもいいし、含めなくてもいい。
- パッケージ名を含めなかった場合は、名前が一致する全てのクラスが対象になる。
インナークラスを指定する
RULE sample
CLASS Main$InnerClass
METHOD method
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
new InnerClass().method();
}
public static class InnerClass {
public void method() {
System.out.println("InnerClass.method()");
}
}
}
Byteman Sample!!
InnerClass.method()
- インナークラスを対象としたい場合は、
<包含するクラス>$<インナークラス名>
とする。
インターフェースを指定する
RULE sample
# ★↓ここ
INTERFACE MyInterface
METHOD method
AT ENTRY
IF true
DO traceln("interface")
ENDRULE
public interface MyInterface {
void method();
}
public class Hoge implements MyInterface {
public void method() {
System.out.println("Hoge");
}
}
public class Fuga implements MyInterface {
public void method() {
System.out.println("Fuga");
}
}
public class Main {
public static void main(String... args) throws Exception {
new Hoge().method();
new Fuga().method();
}
}
interface
Hoge
interface
Fuga
-
INTERFACE
で宣言する。 - 指定したインターフェースを実装したクラスが全て対象になる。
-
CLASS
と同じく、パッケージは省略可能。
メソッドを指定する
RULE sample
INTERFACE MyInterface
# ★↓ここ
METHOD method
AT ENTRY
IF true
DO traceln("interface")
ENDRULE
-
METHOD
で宣言する。 - 戻り値と引数の型は省略可能。
- 戻り値を省略しないで書くと
void method
。 - 引数を省略しないで書くと
method()
になる。- 引数を受け取る場合は
method(String, int, double)
みたいな感じで型名をカンマ区切りで列挙する。
- 引数を受け取る場合は
- 戻り値を省略しないで書くと
コンストラクタを指定する
RULE sample
CLASS Main
# ★ <init> で指定
METHOD <init>
IF true
DO traceln("Byteman Sample!!")
ENDRULE
- コンストラクタを対象にする場合は、
<init>
というふうに指定する。 - 引数も含めて指定する場合は
<init>(String)
という感じで記述する。
サブクラスでオーバーライドされたメソッドも対象にする
デフォルトだと、 CLASS
で指定したクラスにしかルールが適用されず、サブクラスでオーバーライドされたメソッドにはルールが適用されない。
このため、サブクラスでオーバーライドされたメソッドにもルールを適用したい場合は、サブクラスごとにルールを定義しないといけない。
それはあまりにも辛い。
なので、サブクラスでオーバーライドされたメソッドにも一括でルールを適用する特別な設定方法が用意されている。
RULE sample
# ★先頭に ^ をつける
CLASS ^Base
METHOD method
AT ENTRY
IF true
DO traceln("call method()")
ENDRULE
public class Base {
public void method() {
System.out.println("Base.method()");
}
}
public class Hoge extends Base {
@Override
public void method() {
System.out.println("Hoge.method()");
}
}
public class Fuga extends Base {
@Override
public void method() {
System.out.println("Fuga.method()");
}
}
public class Main {
public static void main(String[] args) {
new Base().method();
new Hoge().method();
new Fuga().method();
}
}
call method()
Base.method()
call method()
Hoge.method()
call method()
Fuga.method()
CLASS
で指定するクラス名の先頭に ^
を付ける。
これで、そのクラスのサブクラスでオーバーライドされたメソッドにも、同じルールが適用されるようになる。
親メソッドを呼び出したときの二重適用を防ぐ
例えば super
などで親のメソッドを明示的に呼び出している場合、ルールが二重で適用されることがある。
明示していなくても、デフォルトコンストラクタなどは暗黙的に呼び出されるので、意図せずルールが適用される可能性がある。
親クラスのメソッド呼び出し時はルールを適用させたくない場合は、以下のように呼び出し元のメソッドを検証する方法がある。
RULE sample
CLASS ^Base
METHOD method
AT EXIT
# ★ callerEquals() で呼び出し元のメソッドを検証する
IF NOT callerEquals("method")
DO traceln("call method()")
ENDRULE
public class Hoge extends Base {
@Override
public void method() {
super.method();
System.out.println("Hoge.method()");
}
}
public class Main {
public static void main(String[] args) {
new Hoge().method();
}
}
Base.method()
call method()
Hoge.method()
callerEquals()
メソッドで、呼び出し元のメソッドが対象のメソッドと同じかどうかを検証する。
同じ場合は親メソッドの呼び出しと判断して対象外にする(NOT
で否定している)。
インターフェースに ^
を指定する
INTERFACE
でインターフェースを指定した場合、そのインターフェースを実装したクラス全てが対象になる。
しかし、インターフェースを実装したあるクラスがメソッドを実装していて、さらにそのサブクラスがメソッドをオーバーライドしている場合、サブクラスのメソッドはルール適用の対象外になる。
RULE sample
INTERFACE MyInterface
METHOD method
AT EXIT
IF true
DO traceln("call method()")
ENDRULE
public interface MyInterface {
public void method();
}
public abstract class AbstractClass implements MyInterface {
@Override
public void method() {
System.out.println("AbstractClass.method()");
}
}
public class SubClass extends AbstractClass {
@Override
public void method() {
super.method();
System.out.println("SubClass.method()");
}
}
public class Main {
public static void main(String[] args) {
new SubClass().method();
}
}
AbstractClass.method()
call method()
SubClass.method()
MyInterface
を直接実装した AbstractClass
の method()
メソッドにはルールが適用されているが、 SubClass
でオーバーライドした method()
メソッドにはルールが適用されていない。
標準 API の中では、 List
, AbstractList
, ArrayList
の3つがまさにこの関係になっている。
SubClass
の method()
にもルールを適用せたい場合は、 ^
をインターフェース名の前に付ける。
RULE sample
# ★ 先頭に ^ を追加
INTERFACE ^MyInterface
METHOD method
AT EXIT
IF true
DO traceln("call method()")
ENDRULE
AbstractClass.method()
call method()
SubClass.method()
call method()
SubClass.method()
も対象になっている。
コードを差し込む場所を指定する
デフォルト
RULE sample
CLASS Main
METHOD main
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("main method.");
}
}
Byteman Sample!!
main method.
- 何も指定しない場合、メソッドの先頭に処理が差し込まれる。
-
AT ENTRY
を指定した場合も同様の動作になる。 - 親を持つクラスのコンストラクタの場合、親のコンストラクタが暗黙的に呼ばれた後に実行される。
メソッドの最後に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AT EXIT
IF true
DO traceln("Byteman Sample!!")
ENDRULE
main method.
Byteman Sample!!
-
AT EXIT
と指定すると、メソッドの最後に処理を差し込める。
行番号を指定して差し込む
RULE sample1
CLASS Main
METHOD main
# ★↓ここ
AT LINE 4
IF true
DO traceln("at line 4")
ENDRULE
RULE sample2
CLASS Main
METHOD main
# ★↓ここ
AT LINE 5
IF true
DO traceln("at line 5")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("hoge"); // 3 行目
System.out.println("fuga"); // 4 行目
System.out.println("piyo"); // 5 行目
}
}
hoge
at line 4
fuga
at line 5
piyo
-
AT LINE <行番号>
で、行番号指定の差し込みができる。 - コードを差し込んでも、後ろの行番号がずれることはない。
- メソッドの外になる行番号を指定した場合、エラーにはならず無視される。
- ただし、
0
や-1
のように上方向にズレた位置を指定した場合は、メソッドの先頭でコードが差し込まれる模様。
- ただし、
メンバー変数が読み取られる直前にコードを差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AT READ value
IF true
DO traceln("value is read (x = " + $x + ")")
ENDRULE
public class Main {
private String value = "main value";
public static void main(String[] args) {
String x = null;
Main main = new Main();
System.out.println("1");
x = main.value + " 1";
System.out.println("2");
x = main.value + " 2";
}
}
1
value is read (x = null)
2
-
AT READ <メンバー変数名>
で、指定したメンバー変数が最初に読み取られたときの直前に処理を差し込める。 -
static
な変数も対象になる。 -
$x
はローカル変数x
を参照している。
n 回目に読み取られる直前に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★ 2 を指定
AT READ value 2
IF true
DO traceln("value is read (x = " + $x + ")")
ENDRULE
1
2
value is read (x = main value 1)
- 変数名の後ろに数値を指定することで、指定した回数目に変数が読み取られる直前に処理を差し込める。
- 常に処理を差し込みたい場合は、数値の代わりに
ALL
と指定する。
RULE sample
CLASS Main
METHOD main
# ★ ALL を設定する
AT READ value ALL
IF true
DO traceln("value is read")
ENDRULE
特定のクラスのフィールドに絞り込む
RULE sample
CLASS Main
METHOD main
# ★ 変数名の前にクラス名を追加
AT READ Main$Fuga.value
IF true
DO traceln("Fuga.value is read")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("Hoge");
String xxx = new Hoge().value;
System.out.println("Fuga");
int yyy = Fuga.value;
}
public static class Hoge {
public String value;
}
public static class Fuga {
public static int value;
}
}
Hoge
Fuga
Fuga.value is read
- 変数名の前に
<クラス名>.
を追加することで、特定のクラスのメンバー変数を読み込んだときだけに限定できる。
ローカル変数が読み取られたときの直前にコードを差し込む
RULE sample
CLASS Main
METHOD main
# ★ 変数名の前に $ をつける
AT READ $value
IF true
DO traceln("xxxx = " + $xxxx)
ENDRULE
public class Main {
public static void main(String[] args) {
String xxxx = "hoge";
String value = "fuga";
System.out.println("read value");
xxxx = value;
}
}
read value
xxxx = hoge
- 変数名の前に
$
をつけると、ローカル変数が対象になる。 - メソッド引数も対象になる。
- 回数の指定などはメンバー変数を指定した場合と同じ。
メンバー変数に書き込まれる直前に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★ ここ
AT WRITE value
IF true
DO traceln("write value")
ENDRULE
public class Main {
private String value = "hoge";
public static void main(String[] args) {
System.out.println("write to Main.value");
new Main().value = "fuga";
}
}
write to Main.value
write value
-
AT WRITE
で、メンバー変数に書き込まれる直前に処理を差し込める。 - その他の使い方は
AT READ
のときと同じ。
メソッドが呼ばれる直前に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AT INVOKE method
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("start");
method();
method();
System.out.println("end");
}
public static void method() {
System.out.println("method()");
}
}
start
Byteman Sample!!
method()
method()
end
-
AT INVOKE
で、指定したメソッドが呼び出される直前に処理を差し込むことができる。 - 変数の参照のときと同じで、デフォルトは最初の一回目の呼び出しにだけ適用される。
- メソッド名の後ろに数値や
ALL
を指定することで、同じように調整可能。 - メソッド名の前にクラス名を入れることで、クラスの限定も可能。
- メソッド名の後ろに数値や
メソッドが呼ばれた直後に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AFTER INVOKE method
IF true
DO traceln("Byteman Sample!!")
ENDRULE
start
method()
Byteman Sample!!
method()
end
-
AFTER INVOKE
にすれば、メソッド呼び出しの直後に処理を差し込める。 - その他の使い方は、
AT INVOKE
の場合と同じ。
synchronized
ブロックの前後に処理を差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AT SYNCHRONIZE
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("start");
synchronized(Main.class) {
System.out.println("in synchronized");
}
System.out.println("end");
}
}
start
Byteman Sample!!
in synchronized
end
-
AT SYNCHRONIZE
で、synchronized
ブロックの前に処理を挟める。- デフォルトでは、変数の場合と同じで最初のブロックだけが対象になる。
- 数値または
ALL
で、同じように調整可能。
- 後に処理を挟む場合は、
AFTER SYNCHRONIZE
を使う。 - 差し込まれたコードが
synchronized
ブロックの中で実行されるのか外で実行されるのかは、僕の力量では読み取れませんでした。。。
例外がスローされるときにコードを差し込む
RULE sample
CLASS Main
METHOD main
# ★↓ここ
AT THROW
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
try {
throw new Exception("test");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
Byteman Sample!!
test
-
AT THROW
で、例外がスローされるときに処理を差し込める。- デフォルトでは、変数の場合と同じで最初の
throw
だけが対象になる。 - 数値または
ALL
で、同じように調整可能。
- デフォルトでは、変数の場合と同じで最初の
変数
変数を宣言する
RULE sample
CLASS Main
METHOD method
# ★↓ここ
BIND hoge = "HOGE";
mainCount = $0.count;
IF true
DO traceln("hoge=" + hoge + ", mainCount=" + mainCount)
ENDRULE
public class Main {
public int count;
public static void main(String[] args) {
Main m = new Main();
m.method();
m.method();
}
public void method() {
this.count++;
}
}
hoge=HOGE, mainCount=0
hoge=HOGE, mainCount=1
-
BIND
句を使って変数を定義できる。 - 変数の宣言は
<変数名> = <値>;
で定義できる。- セミコロンは複数行宣言するときの区切りとして必要なので、1行だけの場合や最後の変数宣言では省略が可能。
- 型を明示することもでき、その場合は
<変数名>:<型名> = <値>
とする。
- 変数宣言はルールで指定したコードが実行されるたびに初期化される。
-
$0
は、対象のメソッド (method()
) を持つインスタンスそのもの (Main
のインスタンス) を参照している。 - 宣言した変数は、処理の本体や条件文などで使用することができるようになる。
暗黙的に使用できる変数
BIND
を使って宣言する変数以外に、 $0
のように暗黙的に使用できる変数が用意されている。
変数名 | 参照できる値 |
---|---|
$0 |
実行されたメソッドを持つインスタンス。 |
$n |
メソッドの n 番目の引数。 |
$* , $@
|
実行されたメソッドを持つインスタンスと、メソッドの全引数。 |
$# |
メソッド引数の数。 |
$! |
メソッドの戻り値。 |
$^ |
スローされた例外。 |
$CLASS |
現在のクラス名。 |
$METHOD |
現在のメソッド名。 |
実行されたメソッドを持つインスタンスを参照する
RULE sample
CLASS Main
METHOD method
IF true
DO traceln("$0 = " + $0);
ENDRULE
public class Main {
public static void main(String[] args) {
new Main().method();
}
public void method() {}
}
$0 = Main@4b1210ee
-
$0
で、実行されているメソッドを持つインスタンスを参照できる。 -
static
メソッドが実行されているときに参照した場合はルールが無効になり何も実行されなくなる。 -
$this
でも同じ値を参照できる。
メソッドの引数を参照する
RULE sample
CLASS Main
METHOD method
IF true
DO traceln("$1 = " + $1 + ", $2 = " + $2);
ENDRULE
public class Main {
public static void main(String[] args) {
new Main().method("hoge", 21);
}
public void method(String a, int b) {}
}
$1 = hoge, $2 = 21
-
$<1以上の数値>
で、メソッドの引数を参照できる。 - 引数の数よりも大きい値を参照しようとすると、ルールが無効になって何も実行されなくなる。
メソッドを持つインスタンスと引数を全て参照する
RULE sample
CLASS Main
METHOD method
IF true
DO traceln("$* = " + java.util.Arrays.toString($*));
ENDRULE
$* = [Main@4d7e1886, hoge, 21]
-
$*
で$<0 以上の数値>
で取得できる全ての変数を格納した配列を参照できる。 -
static
メソッド参照した場合は、インデックス0
の部分にnull
が格納される。
AT INVOKE
のときに参照する
RULE sample
CLASS Main
METHOD main
AT INVOKE method
IF true
DO traceln("$@ = " + java.util.Arrays.toString($@));
ENDRULE
$@ = [Main@3cd1a2f1, hoge, 21]
-
AT INVOKE
で指定したメソッドの引数を参照する場合は$@
を使用する。 - こちらも、
static
メソッドを対象にした場合はインデックス0
にnull
が設定される。
メソッド引数の数を参照する
RULE sample
CLASS Main
METHOD method
IF true
DO traceln("$# = " + $#);
ENDRULE
$# = 2
メソッドの戻り値を参照する
RULE sample
CLASS Main
METHOD method
AT EXIT
IF true
DO traceln("$! = " + $!);
ENDRULE
public class Main {
public static void main(String[] args) {
new Main().method();
}
public String method() {
return "RESULT";
}
}
$! = RESULT
-
$!
でメソッドの戻り値を参照できる。 - これが使えるのは
AT EXIT
かAFTER INVOKE
のときだけ。
スローされた例外を参照する
RULE sample
CLASS Main
METHOD main
AT THROW
IF true
DO traceln("$^.message = " + $^.getMessage());
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
throw new Exception("test exception");
}
}
$^.message = test exception
-
$^
で、スローされた例外を参照できる。 - これが使えるのは、
AT THROW
のときだけ。
現在のクラス名を参照する
RULE sample
CLASS Main
METHOD main
IF true
DO traceln("$CLASS = " + $CLASS);
ENDRULE
package sample.byteman;
public class Main {
public static void main(String[] args) {
}
}
$CLASS = sample.byteman.Main
-
$CLASS
で、現在のクラスの FQCN を文字列で参照できる。
現在のメソッド名を参照する
RULE sample
CLASS Main
METHOD main
IF true
DO traceln("$METHOD = " + $METHOD);
ENDRULE
$METHOD = main(java.lang.String[]) void
-
$METHOD
で、現在のメソッドの名前を文字列で参照できる。
条件
RULE sample
CLASS Main
METHOD method
# ★↓ここ
IF $this.flag
DO traceln("Byteman Sample!!");
ENDRULE
public class Main {
private boolean flag;
public static void main(String[] args) throws Exception {
Main main = new Main();
main.method();
main.flag = true;
main.method();
}
public void method() {
System.out.println("Main.method()");
}
}
Main.method()
Byteman Sample!!
Main.method()
-
IF
で指定した条件がtrue
のときだけ処理の差し込みが実行される。 - 常に実行したい場合は
IF true
とすればいい。
論理演算子を英単語で記述する
RULE sample
CLASS Main
METHOD main
IF true AND (false OR true)
DO traceln("Byteman Sample!!");
ENDRULE
Byteman Sample!!
-
&&
はAND
、||
はOR
のように、論理演算子を英単語に置き換えることができる。 - 論理演算子以外にも、比較演算子・算術演算子も英単語に置き換えられる(論理演算子以外は記号の方が可読性高いと思うけど)。
演算子 | 英単語 |
---|---|
|| |
OR |
&& |
AND |
! |
NOT |
<= |
LE |
< |
LT |
== |
EQ |
!= |
NE |
>= |
GE |
> |
GT |
* |
TIMES |
/ |
DIVIDE |
+ |
PLUS |
- |
MINUS |
% |
MOD |
処理
基本
RULE sample
CLASS Main
METHOD main
IF true
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
}
}
Byteman Sample!!
-
DO
句の中で差し込む処理を記述する。 - 処理はセミコロンで区切ることで、複数行にわたって記述することができる。
-
traceln()
は組み込みのメソッドで、標準出力に文字を出力する。
変数の値を変更する
RULE sample
CLASS Main
METHOD main
AFTER READ $value
IF true
DO $value = "byteman"
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
String value = "hoge";
System.out.println("value = " + value);
System.out.println("value = " + value);
}
}
value = hoge
value = byteman
-
<変数名> = <値>
とすることで、普通にコード上の変数の値を書き変えられる(なにこれこわい)。- 今回の例ではローカル変数を対象にしているので
$
を先頭につけている。 - この辺は
AT READ
のときと同じ要領になる。
- 今回の例ではローカル変数を対象にしているので
メソッドの戻り値を差し替える
RULE sample
CLASS Main
METHOD method
AT EXIT
IF true
DO $! = "byteman"
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("method() = " + method());
}
public static String method() {
return "RESULT";
}
}
method() = byteman
-
$!
に値を代入すれば、戻り値を差し替えられる(なにこれすごくこわい)。
また、 return
文を記述することでも戻り値を差し替えられる。
RULE sample
CLASS Main
METHOD method
IF true
DO return "byteman"
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("method() = " + method());
}
public static String method() {
System.out.println("Main.method()");
return "RESULT";
}
}
method() = byteman
-
method()
メソッドの中は実行されず、値が強制的返されている。
組み込みのヘルパークラス
traceln()
などの暗黙的に利用できるメソッドの実体は、 org.jboss.byteman.rule.helper.Helper
に定義されている。
全部調べるのは大変そうなので、目についたやつの使い方を調べる。
カウントダウン
RULE sample 1
CLASS Main
METHOD main
IF true
DO createCountDown("hoge", 3)
ENDRULE
RULE sample 2
CLASS Main
METHOD method
IF countDown("hoge")
DO traceln("Count ZERO")
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
for (int i=0; i<5; i++) {
method();
}
}
public static void method() {
System.out.println("Main.method()");
}
}
Main.method()
Main.method()
Main.method()
Count ZERO
Main.method()
Main.method()
-
createCountDown()
で、カウントダウンするための初期化を行う。- 第一引数に、カウントダウンを識別するための値(
Object
)を渡す。 - 第二引数に、カウントの初期値を設定する。
- 第一引数に、カウントダウンを識別するための値(
-
countDown()
で、カウントを1つ減らす。- 引数には、
createCountDown()
のときに渡した識別子を渡す。 - カウントが
0
になったタイミングでtrue
を返す。 - それ以外は常に
false
を返す。
- 引数には、
フラグ
RULE sample 1
CLASS Main
METHOD flagOn
IF true
DO flag("hoge")
ENDRULE
RULE sample 2
CLASS Main
METHOD testFlagged
IF flagged("hoge")
DO traceln("Flagged!!")
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
testFlagged();
flagOn();
testFlagged();
}
public static void flagOn() {
System.out.println("Main.flagOn()");
}
public static void testFlagged() {
System.out.println("Main.testFlagged()");
}
}
Main.testFlagged()
Main.flagOn()
Flagged!!
Main.testFlagged()
-
flag()
で、フラグを立てる。- 引数には識別するための値を渡す。
-
flagged()
で、フラグが立っているか確認する。- 引数には
flag()
で渡した識別子を渡す。 - フラグが立てられていたら
true
を返す。 - それ以外は
false
を返す。
- 引数には
-
clear()
でフラグをクリアできる。
カウンター
RULE sample 1
CLASS Main
METHOD main
IF true
DO createCounter("hoge")
ENDRULE
RULE sample 2
CLASS Main
METHOD increment
IF true
DO traceln("counter = " + incrementCounter("hoge"))
ENDRULE
RULE sample 3
CLASS Main
METHOD decrement
IF true
DO traceln("counter = " + decrementCounter("hoge"))
ENDRULE
RULE sample 4
CLASS Main
METHOD main
AT EXIT
IF true
DO traceln("counter = " + readCounter("hoge"))
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
increment();
increment();
decrement();
}
public static void increment() {}
public static void decrement() {}
}
counter = 2
counter = 3
counter = 2
counter = 2
-
createCounter()
で、カウンターを作成する。- 第一引数で、カウンターを識別するための値を渡す。
- 第二引数で、カウンターの初期値を渡す(省略した場合は
0
)
-
incrementCounter()
で、カウンターを1インクリメントする。- 引数には識別子を渡す。
-
decrementCounter()
で、カウンターを1デクリメントする。 -
readCounter()
で、カウンターの現在の値を取得する。
タイマー
RULE sample 1
CLASS Main
METHOD main
IF true
DO createTimer("hoge")
ENDRULE
RULE sample 2
CLASS Main
METHOD sleep
AT EXIT
IF true
DO traceln("time = " + getElapsedTimeFromTimer("hoge"));
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
for (int i=1; i<=3; i++) {
sleep(i);
}
}
public static void sleep(int time) throws Exception {
Thread.sleep(time * 100);
}
}
time = 101
time = 301
time = 601
-
createTimer()
で、タイマーを作成する。- 第一引数にはタイマーを識別するための値を渡す。
-
getElapsedTimeFromTimer()
で、現在の経過時間をミリ秒で取得する。 -
resetTimer()
で時間をリセットできる。
再帰的にルールが適用されないようにする
RULE sample 1
CLASS Main
METHOD method
IF true
DO traceln("Byteman Sample!!");
$this.method()
ENDRULE
public class Main {
public static void main(String[] args) {
new Main().method();
}
public void method() {}
}
※なにも出力されずプログラムが終了しなくなる
やや極端だが、ルールの処理のなかでルールの適用対象である method()
が再度呼びだされている。
この場合、ルールの処理内で呼びだされた method()
が再びルールの適用対象となり、無限ループが発生する。
これは意図的なものだが、意図せずこのような状態になることも、なきにしもあらず。
(ユーザーガイドでは、 FileOutputStream
の open()
メソッドを対象にすると traceln()
内で同じメソッドを使っているためこの状態が発生すると説明している)
このような場合に、ルールの処理中で呼びだされたメソッドを対象外にする方法として setTriggering()
というメソッドが用意されている。
RULE sample 1
CLASS Main
METHOD method
IF true
DO setTriggering(false);
traceln("Byteman Sample!!");
$this.method()
ENDRULE
Byteman Sample!!
setTriggering(false)
とすることで、以降に記述した処理はルール適用の対象外になる。
なお、 true
を渡せば再び適用対象に戻るが、ルールの処理が終了すれば勝手に true
に戻るので明示的に true
を設定する必要はない。
デバッグ
RULE sample
CLASS Main
METHOD main
IF true
DO debug("debug message")
ENDRULE
> bmjava -l sample.btm -Dorg.jboss.byteman.debug Main
Default helper activated
Installed rule using default helper : sample
rule.debug{sample_0} : debug message
-
debug()
で、デバッグ用のメッセージ出力ができる。 - デフォルトではメッセージは出力されない。
- メッセージを出力させるには、システムプロパティとして
org.jboss.byteman.debug
を起動時に宣言する必要がある。
トレース
RULE sample
CLASS Main
METHOD main
IF true
DO traceln("trace message")
ENDRULE
trace message
-
traceln()
で、標準出力にメッセージを出力できる。- 改行を入れない
trace()
メソッドもある。
- 改行を入れない
- こちらは何もしなくても常にメッセージが出力される。
ファイルに出力する
RULE sample 1
CLASS Main
METHOD main
IF true
DO traceOpen("hoge")
ENDRULE
RULE sample 2
CLASS Main
METHOD method
IF true
DO traceln("hoge", "arg=" + $1)
ENDRULE
RULE sample 3
CLASS Main
METHOD main
AT EXIT
IF true
DO traceClose("hoge")
ENDRULE
public class Main {
public static void main(String[] args) {
method("foo");
method("bar");
}
public static void method(String value) {}
}
実行すると、標準出力には何も出力されずカレントディレクトリに byteman.log
というファイルが出力される。
arg=foo
arg=bar
-
traceOpen()
で出力ファイルを開く。- 第一引数にファイルを識別するための値を渡す。
- 第二引数にファイル名を渡す。
-
traceln()
の第一引数にファイルを識別するための値を渡すことで、ファイルに出力できる。 -
traceClose()
で開いていたファイルを閉じる。 -
Helper
クラスの実装を覗いたけど、残念ながら文字コードの指定はできない模様。
呼び出し元のメソッド名を検証する
RULE sample
CLASS Main
METHOD method
IF callerEquals("hoge")
DO traceln("Byteman Sample!!")
ENDRULE
public class Main {
public static void main(String[] args) {
System.out.println("call method() from main()");
method();
hoge();
}
public static void hoge() {
System.out.println("call method() from hoge()");
method();
}
public static void method() {
System.out.println("Main.method()");
}
}
call method() from main()
Main.method()
call method() from hoge()
Byteman Sample!!
Main.method()
-
callerEquals()
で、呼び出し元のメソッド名を検証できる。 - 呼び出し元のメソッドの名前が、引数で渡した文字列と一致する場合に
true
を返す。 -
callerEquals()
にはオーバーロードされたメソッドが多数用意されており、いろいろな条件で検証ができる模様。 -
callerMaches()
というメソッドもあり、こちらは正規表現で検証ができる。
スタックトレースを出力する
RULE sample
CLASS Main
METHOD method
IF true
DO traceStack()
ENDRULE
public class Main {
public static void main(String[] args) {
method();
}
public static void method() {}
}
Stack trace for thread main
Main.method(Main.java:-1)
Main.main(Main.java:3)
-
traceStack()
で現在のスタックトレースを出力できる。 -
formatStack()
ならスタックトレースを文字列で取得できる。
正規表現で一致するものだけ出力する
RULE sample
CLASS Main
METHOD five
IF true
DO traceStackMatching(".*e$")
ENDRULE
public class Main {
public static void main(String[] args) {
one();
}
public static void one() {two();}
public static void two() {three();}
public static void three() {four();}
public static void four() {five();}
public static void five() {}
}
Stack trace for thread main matching .*e$
Main.five(Main.java:-1)
Main.three(Main.java:8)
Main.one(Main.java:6)
-
traceStackMatching()
で、指定した正規表現に一致するメソッド名のスタックだけを出力できる。 - こちらも、
formatStackMatching()
という文字列を返すメソッドが用意されている。
ヘルパークラスを自作する
ヘルパークラスは自作することができる。
基本
RULE sample
# ★↓ここ
HELPER sample.byteman.MyHelper
CLASS Main
METHOD main
IF true
DO hello()
ENDRULE
package sample.byteman;
public class MyHelper {
public void hello() {
System.out.println("MyHelper.hello()");
}
}
public class Main {
public static void main(String[] args) {
}
}
MyHelper.hello()
-
HELPER
で自作のヘルパークラスを指定できる。 - ヘルパークラスは POJO で実装できる。
- 自作のヘルパーを読み込むと、デフォルトのヘルパークラスは利用できなくなる。
- なので、 POJO で実装できるとはいっても、事実上デフォルトの
Helper
を継承して作らないと辛い気がする。
- なので、 POJO で実装できるとはいっても、事実上デフォルトの
1つのファイルの中で複数のルールに同じヘルパーを利用できるようにする
ルールごとに HELPER
でヘルパークラスを宣言するのは大変なので、ファイルの先頭で1回だけ宣言する方法も用意されている。
# ★ ここで宣言する
HELPER sample.byteman.MyHelper
RULE rule 1
CLASS Main
METHOD main
IF true
DO println("rule 1")
ENDRULE
RULE rule 2
CLASS Main
METHOD main
IF true
DO println("rule 2")
ENDRULE
package sample.byteman;
public class MyHelper {
public void println(String message) {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
}
}
rule 1
rule 2
ファイルをまたがるのは無理なので、それぞれのファイルの先頭で宣言する必要がある。
ライフサイクルメソッド
ヘルパークラスにはライフサイクルのイベントごとにコールバックされるメソッドを定義することができる。
package sample.byteman;
public class MyHelper {
public void println(String message) {
System.out.printf("[%10d] %s%n", this.hashCode(), message);
}
public static void activated() {
System.out.println("activated");
}
public static void installed(String ruleName) {
System.out.println("installed : " + ruleName);
}
public static void uninstalled(String ruleName) {
System.out.println("uninstalled : " + ruleName);
}
public static void deactivated() {
System.out.println("deactivated");
}
}
RULE rule 1
HELPER sample.byteman.MyHelper
CLASS Main
METHOD method
IF true
DO println("rule 1")
ENDRULE
RULE rule 2
HELPER sample.byteman.MyHelper
CLASS Main
METHOD method
IF true
DO println("rule 2")
ENDRULE
public class Main {
public static void main(String[] args) throws Exception {
int i = 0;
while (true) {
method();
Thread.sleep(3000);
System.out.println(++i);
}
}
public static void method() {}
}
↓動作検証している様子。
- ヘルパーが最初に読み込まれた時点で
activated()
メソッドがコールバックされる。 - ルールに最初に読み込まれるたびに
installed()
メソッドがコールバックされる。 - ルールがアンインストールされるときに
uninstalled()
メソッドがコールバックされる。 - 全てのルールから利用されなくなった時点で
deactivated()
メソッドがコールバックされる。 - 各メソッドは、
static
メソッドとして定義する。 - ヘルパークラスのインスタンス自体は、ルールで呼ばれるたびに再作成されている。
- よって、複数のルールにまたがって値を保存したい場合は
static
変数で宣言する必要がある。
- よって、複数のルールにまたがって値を保存したい場合は
+α
これで終わったら、いつもの記事と変わらなくてつまらないですよね。
せっかくのアドベントカレンダーなので、ちょっと趣向を凝らして Byteman を使ったゲームを考えてみました。
GitHub に上げています。
環境条件
JDK は 8 でお願いします。
一応、 Window と CentOS で動作検証をして動くことは確認してます。
インストール方法
上記 GitHub のページから git clone
するか zip を落とすかしてください。
完了したら、プロジェクトのトップディレクトリで以下のコマンドを実行してください。
> gradlew.bat installDist
$ chmod +x gradlew
$ ./gradlew installDist
実行
> cd build\install\byteman-game\bin
> puzzle.bat 0
> cd build/install/byteman-game/bin
> ./puzzle 0
ゲームが起動します。
そうしたら、新しくコマンドラインを立ち上げ、今度は build/install/byteman-game/rules
に移動し、以下のようにコマンドを実行してください。
> cd build\install\byteman-game\rules
> install.bat level0\1.btm
$ cd build/install/byteman-game/rules
$ chmod +x install uninstall
$ ./install level0/1.btm
コマンドを実行すると、最初に起動しておいた方が SUCCESS
という文字を出力して終了すると思います。
以下は、実際に動かしている様子です。
説明
Byteman を利用したパズルゲームです。
ゲームを起動すると無限ループが始まります。
プレイヤーは、あらかじめ用意された Byteman の設定ファイルを使い、無限ループがうまく抜けられるように設定をインストールし、処理を制御します。
SUCCESS!!
と出力されて終了すれば成功。
例外がスローされて終了した場合は失敗です。
レベル
ゲームはレベルで分けられています。
puzzle
コマンドでゲームを起動するときに、引数でレベルを指定します。
> puzzle.bat 1
とりあえず全部で0~4の5つを用意してみました。
以下が、レベルごとに対応する実装クラスです。
レベル | 対応するクラス |
---|---|
0 | gl8080.byteman.puzzle.level0.Level0 |
1 | gl8080.byteman.puzzle.level1.Level1 |
2 | gl8080.byteman.puzzle.level2.Level2 |
3 | gl8080.byteman.puzzle.level3.Level3 |
4 | gl8080.byteman.puzzle.level4.Level4 |
設定ファイル
設定ファイルはレベルごとにあらかじめ用意されたものを rules
の下に配置しているので、それを使用してください。
また、 rules
ディレクトリの下には設定ファイルをインストール・アンインストールするためのコマンドを用意していますので、それを利用してください。
(Byteman を別途インストールしていて bmsubmit
が使えるのであれば、そちらを使っても構いません)
rules/
|-install - Linux 用のインストールコマンド
|-uninstall - Linux 用のアンインストールコマンド
|-install.bat - Windows 用のインストールコマンド
|-uninstall.bat - Windows 用のアンインストールコマンド
|-level0/ - level0 用の設定ファイル
|-level1/ - level1 用の設定ファイル
|-level2/ - level2 用の設定ファイル
|-level3/ - level3 用の設定ファイル
`-level4/ - level4 用の設定ファイル
中身の説明
puzzle
コマンドでゲームを開始すると、 Main
クラス内で無限ループが開始されます。
package gl8080.byteman.puzzle;
...
public class Main {
...
public void start() throws IllegalParameterException {
PuzzleGame game = this.createGame();
...
boolean flag = true;
while (flag) {
flag = game.step(); // ★ game.step() が false を返すまで無限ループ
this.sleep();
}
...
System.out.println("SUCCESS!! (time = " + this.calcTime() + ")");
}
...
}
この PuzzleGame
はインターフェースで、このインターフェースを実装したクラスがレベルごとに用意されています。
package gl8080.byteman.puzzle.level0;
import gl8080.byteman.puzzle.PuzzleGame;
public class Level0 implements PuzzleGame {
private boolean flag = true;
@Override
public boolean step() {
return this.flag;
}
@Override
public int getLevel() {
return 0;
}
}
これはレベル0の実装です。
単純にインスタンス変数の flag
を返しています。
これに対して用意されている Byteman の設定ファイルが以下です。
RULE level0-1
CLASS Level0
METHOD step
IF true
DO return false
ENDRULE
Level0
クラスの step()
メソッドに入ったら、即座に false
を返却するように実装を書き換える設定になっています。
従って、この設定ファイルをインストールすることで無限ループを終了させることができるわけです。
レベル1以降も、基本的な仕組みは同じです。
各レベルの実装と画面の出力から現在の処理がどう流れているかを考え、用意された設定ファイルにあるルールを適用することでどのように処理が変化するかを想像し、適切な順序でルールのインストールを行っていってください。
以上です、せっかくのアドベントカレンダーなので何か一捻り入れたいなーと思って捻りだした結果がこれです。
なんだか退屈そうなゲームかもしれませんが、楽しんでいただければ幸いです。
(一応解答例を doc/answer.md
に載せてます)