home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Java/clas3hift

Java/clas3hift

Java / clas3hift
id: 458 所有者: msakamoto-sf    作成日: 2009-10-12 16:03:21
カテゴリ: Java 

clas3hiftのプロジェクトはクローズします(2013-07)

バイトコードの動的な生成処理とClassLoaderを組み合わせる技法は、JVMに対する高度かつ深い知識が必要で、しかもトラブルが発生した場合に非常にデバッグや解決が難しい場合が多々あります。
OpenSourceで実際の開発で耐えうるプロジェクトが存在することもあり、clas3hiftはクローズします。

  • アプリ内でクラスローダを分割して、個別にjarファイルなどを指定してコンテナ環境を作りたい場合:
  • JUnitなどの単体テストで、staticメソッドや System ClassLoader によりロードされるJDKコアクラスをmockしたい場合:

ただし、 技術/TDD/Javaでstaticメソッドをmockする にも書きましたがClassLoader周りはひとつ間違えると、非常にデバッグや解決が難しいトラブルが発生します。可能な限り、ClassLoaderを操作する以外の妥協策を探ることを推奨します。また、まだ勉強中ですがアプリ内でクラスローダを分割してコンテナ化したい用途であればOSGiを検討してみても良いかもしれません。

万が一ClassLoaderを操作しなければならない場面に遭遇したら、BCELやJavassist, さらにobjenesisなどのシリアライズライブラリを組み合わせた非常に高度な技法を駆使する必要があります。そのような自体にならないことを切に祈念します。


clas3hiftの紹介

clas3hiftとは

EasyMock( http://easymock.org/) やjMock( http://www.jmock.org/ ) がインスタンス渡しを行う場合にMockを作るのに対し、clas3hiftはクラス自体を実行時に動的に差し替える作業をサポートする。

ただし現時点(2009/10)では差し替える事の出来るクラスに対して厳しい制限がある。

  • newされないこと。newするとクラスの参照ループが発生してしまいJVMがハングアップする(爆。
  • staticフィールドを使わない事。確実にクラス未定義系のErrorが発生する(爆。
  • staticメソッド内で、さらに自分自身のstaticメソッドを使わない事。(試していないが確実にクラス/メソッド未定義系の以下略)
  • ClassA.m1()内からClassB.m2()を呼び出している場合、ClassAとClassBの両方を差し替える事は出来ない。(試していないが確実に以下略)

使い方

前提としてclas3hiftのjarにCLASSPATHを通しておく。JUnitのテストコードを例に挙げる。

まず junit.extensions.clas3hift.SandBox インターフェイスを実装したクラスを用意する。ここに実際のテストコードを記述する。実際にclas3hiftのソースツリーに含まれている test/SandBox1.java を示す。

package test;
 
import static org.junit.Assert.*;
import junit.extensions.clas3hift.SandBox;
import java.util.Map;
import test.stub.Foo0;
 
public class SandBox1 implements SandBox {
    public void sandbox(Map<String, Object> result) {
        assertEquals("My name is Foo1", Foo0.getName());
    }
}

test.stub.Foo0 というのが差し替え対象のクラスであり、プロダクションコードで実際に使われるクラスになる。
他に test.stub.Foo1 というのを用意してあり、狙いとしてはこのSandBox1.sandbox()が実行される時には"Foo0"のアクセスが"Foo1"に裏側で切り替わるイメージになる。
上のassertに出てくる "My name is Foo1" というのがFoo1側の返す値で、Foo0の場合は "My name is Foo0" が返されるようになっている。

また、差し替えるテスト用のクラスは、差し替え元のクラスを継承している必要がある。

"Map<String, Object> result"というのは、実行時に後述のClas3hiftから渡されるHashMapで、sandbox中で直接assertするのではなく、sandboxの外でassertしたい場合に使う。具体的にはsandbox中で外部でassertしたい値をresultにセットしておけば、Clas3hift.result(String key) で取り出せるのでそれを使って外部でassertする。

このSandBox1はテストクラス側でインスタンス化し、clas3hiftの提供する特殊なClassLoaderから実行される。テストコード例を test/TestClas3hift.java から抜粋して示す。

package test;
 
import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.*;
import junit.extensions.clas3hift.SandBox;
import junit.extensions.clas3hift.Clas3hift;
 
import java.util.Map;
import test.stub.Foo0;
 
public class TestClas3hift {
 
    @Test
        public void Clas3hift_1() throws ClassNotFoundException {
            Clas3hift cs = new Clas3hift();
            cs.register("test.stub.Foo0", "test.stub.Foo1");
            SandBox1 sb = new SandBox1();
            cs.play(sb);
        }
    // ....
}

まず、clas3hiftのSandBox実行環境である Clas3hiftのインスタンスを生成する。

Clas3hift cs = new Clas3hift();

続いて、SandBox実行中に差し替えるクラスを、FQDNで指定する。元のコード中で使われるクラス名、テスト用に使いたいクラス名の順で指定知る。

cs.register("test.stub.Foo0", "test.stub.Foo1");

SandBoxインターフェイスを実装したクラスのインスタンスを生成する。

SandBox1 sb = new SandBox1();

Clas3hiftのplay(SandBox)メソッドにより、SandBoxが実行される。

cs.play(sb);

SandBoxの制限

現時点(2009/10)では、SandBoxインターフェイスの実装方法には以下の制限がある。

引数無しのコンストラクタしか受け付けられない。

コンストラクタを省略する事によるデフォルトコンストラクタか、引数無しのコンストラクタしか使えない。これはClas3hiftの内部実装の制限による。

インナークラス・ネストクラスの記法が使えない。

インナークラス・ネストクラスにすると、Javaコンパイラにより外側のクラス・インスタンスへアクセスする為のメンバや、コンストラクタが自動的に追加される。
引数無しのコンストラクタしか受け付けられない為、よってインナークラス・ネストクラスとして定義する事は出来ない。

例:次のような書き方は出来ない。(本当はこれがしたかったんだけど・・・)

cs.play(new SandBox() {
    public void sandbox(Map result) {
        assertEquals("I am Foo1", Foo0.name);
        assertEquals("My name is Foo1", Foo0.getName());
    
});

Clas3hiftの仕組み

現時点(2009/10)での内部実装を簡単に説明する。

クラス差し替えの基本的な考え方

Clas3hiftの独自クラスローダである junit.extensions.clas3hift.CL クラスの register() メソッドでクラスの差し替えが実装されている。
まず、差し替え元のクラスのバイトコードを取得し、クラス名をランダムで衝突しにくい名前にしてdefineClass()によりロードする。

test.stub.Foo0 → test.stub.Foo0$(衝突しにくいランダムコード)

次に差し替え先のクラスのバイトコードを取得する。そして以下の処理を行った後、defineClass()によりロードする。

  1. クラス名を差し替え元のクラス名にする。
  2. 親クラス名を、上で生成した差し替え元の新しいクラス名にする。
(クラス名) test.stub.Foo1 → test.stub.Foo0
(親クラス名) test.stub.Foo0 → test.stub.Foo0$(衝突しにくいランダムコード)

これにより、独自クラスローダ内では "test.stub.Foo0"のstaticメソッドの実行が test.stub.Foo1 の該当staticメソッドに入れ替わる事になる。

SandBox実行環境の考え方

もし将来的にSandBoxがインナークラス・ネストクラスで定義できるようになったとしても、そのインスタンスがテストメソッドから生成される以上、そのクラスローダもテスト実行環境のクラスローダになり、Clas3hift独自のクラスローダにはならない。

public void Clas3hift_1() throws ClassNotFoundException {
    System.out.println(this.getClass().getClassLoader()); // (1)
    SandBox1 sb = new SandBox1();
    System.out.println(sb.getClass.getClassLoader()); // (2)

上のコードの (1), (2) は同じクラスローダになり、よって、そのままではクラスの差し替えは行えない。

ここでも独自クラスローダによるトリックが行われる。
独自クラスローダにより一旦SandBoxインスタンスのクラスがコピーされ、独自クラスローダにより再定義されたクラスでSandBoxインスタンスが再生成される。
最終的に実行されるのは、この独自クラスローダにより再定義されたSandBoxクラスであるため、これによりSandBox内のクラスの差し替えが行われる事になる。

cs.play(sb);


public void play(SandBox sb) {
    SandBox sb_ = this._cl.surround(sb);
    sb_.sandbox(this._results);
}

"this._cl" は junit.extensions.clas3hift.CL のインスタンスである。

public SandBox surround(SandBox sb) 
{
    // 元のSandBoxのClassオブジェクトを取得
    Class srcClass = sb.getClass();
    SandBox newSb = null;
    try {
        // この内部で、srcClassをコピーし、再定義(defineClass)している。
        Class newClass = this.cloneClass(srcClass);
        /* if sandbox is defined as innerclass, its constructor 
         * is private. so, powered by PA, call private constuctor.
         */
        newSb = (SandBox)PA.instantiate(newClass, new Object[] {});
        // これにより、クラスローダが独自CLのSandBoxインスタンスが作られる。
    } catch (Throwable t) {
        t.printStackTrace();
    }
    return newSb;
}

最終的にこのnewSbのsandbox()メソッドの実行により、クラス差し替えが裏で行われる。

SandBox実装クラスの制限は、この新しいSandBoxクラスのインスタンス生成が現時点では、単純な引数無しでしか対応できていない事による。
PAライブラリを使っているのは、将来的にインナークラス・ネストクラス対応した場合に、protected/private/packageアクセスのコンストラクタでも対応可能にする為である・・・というのは建前で、本当は最初はstatic main内で実験していた為インナークラス・ネストクラスで実装していて、それで使っていた。JUnitのテストコードに載せた時に初めて、インナークラス・ネストクラスの特殊なコンストラクタに気づいた次第・・・。

参考資料

ClassLoader関連

BCEL関連

インナークラス・ネストクラス関連



プレーンテキスト形式でダウンロード
現在のバージョン : 4
更新者: msakamoto-sf
更新日: 2013-07-27 22:09:36
md5:2b5b27f18a6ec04029d241125772a333
sha1:f33707bfffac2d4863c3b07bd72b1e821163f77d
コメント
コメントを投稿するにはログインして下さい。