torutkのブログ

ソフトウェア・エンジニアのブログ

JBoss Bytemanを使ってAOP

9月26日のJJUG主催ナイトセミナー JJUG ナイトセミナー - torutkの日記 で、「イマドキの現場で使えるJavaライブラリ事情」の紹介にあったBytemanをJUnitから使い、モック的に利用する流れを簡単に記述しました。

Bytemanの設定

Bytemanの公式ホームページ(次のURL)から[Downloads]を辿ってbyteman binary + docsをダウンロードします。本日時点でバージョンは2.1.0です。

ダウンロードしたファイルbyteman-download-2.1.0-bin.zipを展開します(例:C:\java\byteman-download-2.1.0)。

Java開発環境(NetBeans)の設定

NetBeans IDE 7.2の上でJUnitテストケースからBytemanを使用します。まず、Bytemanのライブラリを設定します。
[ツール]メニュー>[Antライブラリ]を選択、[新規ライブラリ]で
ライブラリ名[Byteman]を作成します。クラスパスに次のJARを追加します。

  • C:\java\byteman-download-2.1.0\lib\byteman.jar
  • C:\java\byteman-download-2.1.0\lib\byteman-install.jar
  • C:\java\byteman-download-2.1.0\lib\byteman-submit.jar
  • C:\java\byteman-download-2.1.0\contrib\bmunit\byteman-bmunit.jar

Bytemanを使ってJUnitテストケースを実行するプロジェクトを選択し、[プロパティ]>[ライブラリ]>[テストをコンパイル]を選択、[ライブラリの追加]で先ほど作成した[Byteman]を追加します。また、Bytemanの実行時には、JDKのtools.jarに含まれるクラスを使用しているので、[JAR/フォルダの追加]でJDKインストールディレクトリ/lib/tools.jarを追加します。

テストケースの記述でBytemanを使用

テストケースクラスに@RunWithアノテーションを記述します。

import org.jboss.byteman.contrib.bmunit.BMUnitRunner;
import org.junit.runner.RunWith;

@RunWith(BMUnitRunner.class)
public class HelloBytemanTest {

次に、Bytemanで細工を入れたいテストケースメソッドに、@BMRuleアノテーションでインストゥルメントする処理の定義を記述します。

    @BMRule(name="skip finalize method call",
            targetClass="FileOutputStream",
            targetMethod="finalize()",
            condition="true",
            action="System.out.println(\"skip finalize\"); return")
    @Test
    public void openCloselessメソッドを繰り返し呼ぶ試験() throws Exception {

この例では、このメソッドのテストケース実行中にjava.io.FileOutputStreamのfinalizeメソッドコールが発生したときは、常に(condition="true")、アクション(action)を実行します。

テストケース本体は通常のJUnitテストケース記述です。

        System.out.println("openCloseless");
        HelloByteman instance = new HelloByteman();
        boolean expResult = true;
        for (int i = 0; i < 1000; ++i) {
            String path = String.format("oyo-%04d", i);
            boolean result = instance.openCloseless(path);
            assertEquals(expResult, result);
        }
        TimeUnit.SECONDS.sleep(10);
        System.gc();
        System.out.println("GC実行!");
        TimeUnit.SECONDS.sleep(10);
    }

openCloselessメソッドの中でFileOutputStreamをnewし、close()をしないでリターンさせています。(リソースリークのバグを再現)

FileOutputStreamはfinalizeメソッドの中でcloseを実行しています。そのため、明示的にcloseを呼び出し忘れても、GCが発生後finalizeが実行された時点でファイルリソース(ファイルディスクリプタ)が解放されます。

上述コードはあまりいい書き方ではありませんが(リソース解放忘れをアサートしていない、テストケース内でsleepしている、などJUnit的にはアンチパターン)、テストケース中でSystem.gc()を実行するまでの間、1000個のファイルディスクリプタを握り続けます。その後、"skip finalize"がずらっと表示されます。