#navi_header|Java| clas3hiftの紹介 - clas3hift, Apache License 2.0 -- http://code.google.com/p/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 を示す。 #code|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 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 result"というのは、実行時に後述のClas3hiftから渡されるHashMapで、sandbox中で直接assertするのではなく、sandboxの外でassertしたい場合に使う。具体的にはsandbox中で外部でassertしたい値をresultにセットしておけば、Clas3hift.result(String key) で取り出せるのでそれを使って外部でassertする。 このSandBox1はテストクラス側でインスタンス化し、clas3hiftの提供する特殊なClassLoaderから実行される。テストコード例を test/TestClas3hift.java から抜粋して示す。 #code|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()によりロードする。 + クラス名を差し替え元のクラス名にする。 + 親クラス名を、上で生成した差し替え元の新しいクラス名にする。 (クラス名) 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 のインスタンスである。 #code|java|> 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のテストコードに載せた時に初めて、インナークラス・ネストクラスの特殊なコンストラクタに気づいた次第・・・。 #navi_footer|Java|