52
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaAdvent Calendar 2015

Day 14

Byteman 使い方メモ+α

Last updated at Posted at 2015-12-13

この記事は、Java Advent Calendar 2015 の 14 日目の記事です。
昨日は @skrb さんの JRE をカスタマイズ - jlink でした。
明日は @yukung さんです。

最近存在を知ったので、使い方を調べてみました。

最後に も用意してますので、よければ見てやってください。

環境

OS

Windows 7 64bit SP1

Java

1.8.0_65

Byteman とは

Byteman - JBoss Community

読みは、たぶん「ばいとまん」。

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 プログラムを作る。

Main.java
public class Main {
    public static void main(String... args) {
        System.out.println("Hello World!!");
    }
}

Hello World するだけの簡単なプログラム。

Byteman の設定ファイルを作る

差し込むコードを定義する、 Byteman の設定ファイルを作成する。

hello.btm
RULE hello byteman
CLASS Main
METHOD main
AT ENTRY
IF true
DO traceln("Hello Byteman!!")
ENDRULE

実行する

まずは Main.java をコンパイルして。

Main.javaのコンパイル
> javac Main.java

Byteman の設定ファイル(hello.btm)を指定して Main を実行する。

Bytemanを使って実行
> 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を使った場合
> bmjava -l hello.btm Main
Hello Byteman!!
Hello World!!

複数の設定ファイルを指定する

メインメソッドの後で See you Byteman!! と出力する see-you.btm を作成して、 hello.btm と合わせて実行してみる。

see-you.btm
RULE see you byteman
CLASS Main
METHOD main
AT EXIT
IF true
DO traceln("See you Byteman!!")
ENDRULE

まずは、 -javaagent を明示する方法で。

hello.btmとsee-you.btmを2つ指定する(javaコマンド)
> java -javaagent:%BYTEMAN_HOME%\lib\byteman.jar=script:hello.btm,script:see-you.btm Main
Hello Byteman!!
Hello World!!
See you Byteman!!

カンマ (,) 区切りで script:<設定ファイル名> の記述を続けることで複数の設定ファイルを読み込ませることができる。

次に bmjava を使った場合。

hello.btmとsee-you.btmを2つ指定する(bmjavaコマンド)
> bmjava -l hello.btm -l see-you.btm Main
Hello Byteman!!
Hello World!!
See you Byteman!!

-l オプションを複数宣言すればいい。

動作中のプログラムにルールを動的に追加・削除する

Byteman によるコードの差し込みは、動作中のプログラムに対しても実行できる。

ここでは、以下のプログラムを走らせて、動的にルールを追加したり削除したりしてみる。

Main.java
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 と出力される。

以下は実際に動いている時の様子。

byteman.gif

ちなみに、 bmsubmit コマンドは、裏では以下の Java プログラムを実行している。

bmsubmitの実体
> java -classpath %BYTEMAN_HOME%\lib\byteman-submit.jar org.jboss.byteman.agent.submit.Submit

ルールを追加する

以下のルールを追加してみる。

print.btm
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

実際に動かしたときの様子を見ると分かりやすい。

byteman.gif

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

実際に動いている様子は以下。

byteman.gif

コマンドを実行した直後に print(...) の出力が止まったのが分かる。

bmsubmit -l を再度実行すると、ルールが全てなくなり再び no rules installed と出力されている。

既に起動しているプログラムにリスナーを追加する

リスナーを指定せずに起動されたプログラムに対して、後からリスナーを追加することができる。

まずは普通にプログラムを起動する。

Javaプログラムを起動する
> java Main

次に別のコマンドラインを立ち上げ、先ほど起動した Java プログラムのプロセス ID を調べる。

JavaプログラムのプロセスIDを調査する
> jps
1584 Main
5292 Jps

プロセスID が分かったら、 bminstall コマンドでリスナーを追加する。

リスナーを追加する
> bminstall 1584

bminstall の引数には、先ほど調べた Java プログラムのプロセス ID を指定する。

これでリスナーが登録できたので、あとは bmsubmit でルールの追加・削除ができるようになる。

実際に動かした様子が以下。

byteman.gif

ブートストラップクラスローダに読み込まれるクラスにコードを差し込む

java.lang.Stringjava.math.BigInteger など、ブートストラップクラスローダによって読み込まれるクラスは、そのままではコードの差し込みができない。

toString.btm
RULE hook toString
CLASS BigInteger
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
Main.java
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 のコードがそれぞれブートストラップクラスローダとシステムクラスローダによってロードされているために起こってるらしい。

その辺の仕組み的な話は以下で解説されています。

ブートストラップクラスローダがロードするクラスにもコードを差し込みたい場合は、以下のように -javaagent を設定する必要がある。

ブートストラップクラスローダにBytemanを読み込ませる
> 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 以下のクラスに対してはコードの差し込みが無視されるようになっている。

toString.btm
RULE hook toString
CLASS String
METHOD toString()
AT ENTRY
IF true
DO traceln("toString() is called")
ENDRULE
Main.java
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を使った場合
> 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
Main.java
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
MyInterface.java
public interface MyInterface {
    void method();
}
Hoge.java
public class Hoge implements MyInterface {
    public void method() {
        System.out.println("Hoge");
    }
}
Fuga.java
public class Fuga implements MyInterface {
    public void method() {
        System.out.println("Fuga");
    }
}
Main.java
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
Base.java
public class Base {
    public void method() {
        System.out.println("Base.method()");
    }
}
Hoge.java
public class Hoge extends Base {
    @Override
    public void method() {
        System.out.println("Hoge.method()");
    }
}
Fuga.java
public class Fuga extends Base {
    @Override
    public void method() {
        System.out.println("Fuga.method()");
    }
}
Main.java
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
Hoge.java
public class Hoge extends Base {
    @Override
    public void method() {
        super.method();
        System.out.println("Hoge.method()");
    }
}
Main.java
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
MyInterface.java
public interface MyInterface {
    public void method();
}
AbstractClass.java
public abstract class AbstractClass implements MyInterface {
    @Override
    public void method() {
        System.out.println("AbstractClass.method()");
    }
}
SubClass.java
public class SubClass extends AbstractClass {
    @Override
    public void method() {
        super.method();
        System.out.println("SubClass.method()");
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        new SubClass().method();
    }
}
実行結果
AbstractClass.method()
call method()
SubClass.method()

MyInterface を直接実装した AbstractClassmethod() メソッドにはルールが適用されているが、 SubClass でオーバーライドした method() メソッドにはルールが適用されていない。
標準 API の中では、 List, AbstractList, ArrayList の3つがまさにこの関係になっている。

SubClassmethod() にもルールを適用せたい場合は、 ^ をインターフェース名の前に付ける。

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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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

Main.java
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

Java 実装

実行結果
$* = [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

Java 実装

実行結果
$@ = [Main@3cd1a2f1, hoge, 21]
  • AT INVOKE で指定したメソッドの引数を参照する場合は $@ を使用する。
  • こちらも、 static メソッドを対象にした場合はインデックス 0null が設定される。

メソッド引数の数を参照する

RULE sample
CLASS Main
METHOD method
IF true
DO traceln("$# = " + $#);
ENDRULE

Java 実装

実行結果
$# = 2

メソッドの戻り値を参照する

RULE sample
CLASS Main
METHOD method
AT EXIT
IF true
DO traceln("$! = " + $!);
ENDRULE
Main.java
public class Main {
    public static void main(String[] args) {
        new Main().method();
    }
    
    public String method() {
        return "RESULT";
    }
}
実行結果
$! = RESULT
  • $! でメソッドの戻り値を参照できる。
  • これが使えるのは AT EXITAFTER INVOKE のときだけ。

スローされた例外を参照する

RULE sample
CLASS Main
METHOD main
AT THROW
IF true
DO traceln("$^.message = " + $^.getMessage());
ENDRULE
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
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
Main.java
public class Main {
    public static void main(String[] args) {
        new Main().method();
    }
    
    public void method() {}
}
実行結果
※なにも出力されずプログラムが終了しなくなる

やや極端だが、ルールの処理のなかでルールの適用対象である method() が再度呼びだされている。
この場合、ルールの処理内で呼びだされた method() が再びルールの適用対象となり、無限ループが発生する。

これは意図的なものだが、意図せずこのような状態になることも、なきにしもあらず。
(ユーザーガイドでは、 FileOutputStreamopen() メソッドを対象にすると 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
Main.java
public class Main {
    public static void main(String[] args) {
        method("foo");
        method("bar");
    }
    
    public static void  method(String value) {}
}

実行すると、標準出力には何も出力されずカレントディレクトリに byteman.log というファイルが出力される。

byteman.log
arg=foo
arg=bar
  • traceOpen() で出力ファイルを開く。
    • 第一引数にファイルを識別するための値を渡す。
    • 第二引数にファイル名を渡す。
  • traceln() の第一引数にファイルを識別するための値を渡すことで、ファイルに出力できる。
  • traceClose() で開いていたファイルを閉じる。
  • Helper クラスの実装を覗いたけど、残念ながら文字コードの指定はできない模様。

呼び出し元のメソッド名を検証する

RULE sample
CLASS Main
METHOD method
IF callerEquals("hoge")
DO traceln("Byteman Sample!!")
ENDRULE
Main.java
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
Main.java
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
Main.java
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
MyHelper.java
package sample.byteman;

public class MyHelper {
    
    public void hello() {
        System.out.println("MyHelper.hello()");
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
    }
}
実行結果
MyHelper.hello()
  • HELPER で自作のヘルパークラスを指定できる。
  • ヘルパークラスは POJO で実装できる。
  • 自作のヘルパーを読み込むと、デフォルトのヘルパークラスは利用できなくなる。
    • なので、 POJO で実装できるとはいっても、事実上デフォルトの Helper を継承して作らないと辛い気がする。

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
MyHelper.java
package sample.byteman;

public class MyHelper {
    
    public void println(String message) {
        System.out.println(message);
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
    }
}
実行結果
rule 1
rule 2

ファイルをまたがるのは無理なので、それぞれのファイルの先頭で宣言する必要がある。

ライフサイクルメソッド

ヘルパークラスにはライフサイクルのイベントごとにコールバックされるメソッドを定義することができる。

MyHelper.java
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");
    }
}
rule1.btm
RULE rule 1
HELPER sample.byteman.MyHelper
CLASS Main
METHOD method
IF true
DO println("rule 1")
ENDRULE
rule2.btm
RULE rule 2
HELPER sample.byteman.MyHelper
CLASS Main
METHOD method
IF true
DO println("rule 2")
ENDRULE
Main.java
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() {}
}

↓動作検証している様子。

byteman.gif

  • ヘルパーが最初に読み込まれた時点で activated() メソッドがコールバックされる。
  • ルールに最初に読み込まれるたびに installed() メソッドがコールバックされる。
  • ルールがアンインストールされるときに uninstalled() メソッドがコールバックされる。
  • 全てのルールから利用されなくなった時点で deactivated() メソッドがコールバックされる。
  • 各メソッドは、 static メソッドとして定義する。
  • ヘルパークラスのインスタンス自体は、ルールで呼ばれるたびに再作成されている。
    • よって、複数のルールにまたがって値を保存したい場合は static 変数で宣言する必要がある。

これで終わったら、いつもの記事と変わらなくてつまらないですよね。

せっかくのアドベントカレンダーなので、ちょっと趣向を凝らして Byteman を使ったゲームを考えてみました。

GitHub に上げています。

環境条件

JDK は 8 でお願いします。

一応、 Window と CentOS で動作検証をして動くことは確認してます。

インストール方法

上記 GitHub のページから git clone するか zip を落とすかしてください。

完了したら、プロジェクトのトップディレクトリで以下のコマンドを実行してください。

Windowsの場合
> gradlew.bat installDist
Linuxの場合
$ chmod +x gradlew

$ ./gradlew installDist

実行

Windowsの場合
> cd build\install\byteman-game\bin

> puzzle.bat 0
Linuxの場合
> cd build/install/byteman-game/bin

> ./puzzle 0

ゲームが起動します。
そうしたら、新しくコマンドラインを立ち上げ、今度は build/install/byteman-game/rules に移動し、以下のようにコマンドを実行してください。

Windowsの場合
> cd build\install\byteman-game\rules

> install.bat level0\1.btm
Linuxの場合
$ cd build/install/byteman-game/rules

$ chmod +x install uninstall

$ ./install level0/1.btm

コマンドを実行すると、最初に起動しておいた方が SUCCESS という文字を出力して終了すると思います。

以下は、実際に動かしている様子です。

byteman.gif

説明

Byteman を利用したパズルゲームです。

ゲームを起動すると無限ループが始まります。
プレイヤーは、あらかじめ用意された Byteman の設定ファイルを使い、無限ループがうまく抜けられるように設定をインストールし、処理を制御します。

SUCCESS!! と出力されて終了すれば成功。
例外がスローされて終了した場合は失敗です。

レベル

ゲームはレベルで分けられています。

puzzle コマンドでゲームを起動するときに、引数でレベルを指定します。

レベル1で起動する
> 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 クラス内で無限ループが開始されます。

Main.java
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 はインターフェースで、このインターフェースを実装したクラスがレベルごとに用意されています。

Level0.java
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 の設定ファイルが以下です。

level0/1.btm
RULE level0-1
CLASS Level0
METHOD step
IF true
DO return false
ENDRULE

Level0 クラスの step() メソッドに入ったら、即座に false を返却するように実装を書き換える設定になっています。

従って、この設定ファイルをインストールすることで無限ループを終了させることができるわけです。

レベル1以降も、基本的な仕組みは同じです。

各レベルの実装と画面の出力から現在の処理がどう流れているかを考え、用意された設定ファイルにあるルールを適用することでどのように処理が変化するかを想像し、適切な順序でルールのインストールを行っていってください。

以上です、せっかくのアドベントカレンダーなので何か一捻り入れたいなーと思って捻りだした結果がこれです。
なんだか退屈そうなゲームかもしれませんが、楽しんでいただければ幸いです。
(一応解答例を doc/answer.md に載せてます)

参考

52
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?