WEBワークショップ

HOME > WEBワークショップ > Javassistチュートリアル



Javassistチュートリアル


3.クラスローダ

変更すべきクラスがわかっている場合、そのクラスを変更するもっとも簡単な方法は以下のようにすることです。

  • 1. ClassPool.get() を呼び出して CtClass オブジェクトを取得する。
  • 2. CtClass オブジェクトを変更する。
  • 3. CtClass オブジェクトの writeFile() や toBytecode() をコールする。

そのクラスを変更するかどうかがロード時に決まる場合、Javassist をクラスローダと連携させる必要があります。Javassist はバイトコードのロードと同時に変更できるように、クラスローダとの連携ができるようになっています。Javassist の利用者は Javassist が提供するクラスローダの代わりに、自分で作成したクラスローダを利用することもできます。

●3.1 CtClass における toClass メソッド

CtClass は toClass() という便利なメソッドを提供しています。toClass() は現在のスレッドにおけるクラスローダに CtClass オブジェクトが表すクラスをロードするように要求します。このメソッドを呼び出すには、適切な権限が必要です。権限がない場合は SecurityException がスローされます。

以下のプログラムは toClass() メソッドの使い方を示したものです。

 public class Hello {
     public void say() {
         System.out.println("Hello");
     }
 }

 public class Test {
     public static void main(String[] args) throws Exception {
         ClassPool cp = ClassPool.getDefault();
         CtClass cc = cp.get("Hello");
         CtMethod m = cc.getDeclaredMethod("say");
         m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
         Class c = cc.toClass();
         Hello h = (Hello)c.newInstance();
         h.say();
     }
 }


Test.main() メソッドは Hello クラスの say() メソッドに println() メソッドの呼び出しを追加します。その後、変更された Hello クラスのインスタンスを生成し、say() メソッドを呼び出しています。

このプログラムでは、toClass() メソッドが実行される前に Hello クラスが読み込まれていないという前提が必要であることに注意してください。もしそうでなければ、 JVM は更新された Hello クラスを読み込むために toClass() メソッドが要求するよりも先にオリジナルの Hello クラスを読み込んでしまいます。その結果、変更された Hello クラスの読み込みは失敗してしまうのです(LinkageError がスローされます)。例えば、Test クラスの main() メソッドが以下のようになっている場合です。

 public static void main(String[] args) throws Exception {
     Hello orig = new Hello();
     ClassPool cp = ClassPool.getDefault();
     CtClass cc = cp.get("Hello");
         :
 }


mainメソッドの最初の1行目でオリジナルの Hello クラスが読み込まれているため、クラスローダは2つの異なるバージョンのクラスを同時に読み込むことができず、toClass() メソッドは例外をスローしてしまいます。

プログラムが JBoss や Tomcat のようなアプリケーションサーバ上で動作している場合、toClass() メソッドが使用するクラスローダは適切ではないでしょう。このような場合、意図しない ClassCastException が発生します。例外の発生を避けるためには、toClass() メソッドに適切なクラスローダを渡してやらなければなりません。たとえば bean があなたの作成した セッション Bean のオブジェクトだとすると、次のようなコードなら動作します。

 CtClass cc = ...;
 Class c = cc.toClass(bean.getClass().getClassLoader());


ここではあなたのプログラムをロードしたクラスローダを toClass() メソッドに渡さなければなりません。(上の例では bean オブジェクトを読み込んだクラスローダということになります)

toClass() メソッドは便利に作られてはいますが、より高度な機能が必要な場合は独自のクラスローダを作成した方が良いでしょう。

●3.2 Javaにおけるクラスローディング

Javaでは複数のクラスローダが共存可能であり、それぞれのクラスローダが独自の名前空間を持っています。同じクラス名でもクラスローダが異なっていれば別々のクラスを読み込むことができるのです。ロードされた2つのクラスは異なるクラスと見なされます。この仕様は、クラス名は同じでも実体は異なるような複数のアプリケーションプログラムを同一 JVM 上で動作させることができるということを意味しています。

 ClassPool parent = ClassPool.getDefault();
 ClassPool child = new ClassPool(parent);
 child.insertClassPath("./classes");

注意:
JVM はクラスのダイナミックな再ロードを許可していません。一度クラスローダがクラスを読み込んだら、動作中に更新されたバージョンのクラスを再ロードすることはできないのです。そのため、JVM がクラスをロードしたあとにその定義を変更することはできません。しかし、JPDA(Java Platform Debugger Architecture)は制限つきながらもクラスの再ロード機能を提供しています。詳細は JPDA の "HotSwap" の項を参照してください。


2つの別々なクラスローダによって同じクラスファイルが読み込まれた場合、JVM は同じクラス名とクラス定義に対して2つの異なるクラスを生成します。それら2つのクラスは異なるクラスであると見なされるのです。2つのクラスが同一でないため、片方のクラスのインスタンスをもう片方のクラスの変数へ代入することができません。2つのクラスの間でキャストを行うことはできず、ClassCastException がスローされるのです。

 MyClassLoader myLoader = new MyClassLoader();
 Class clazz = myLoader.loadClass("Box");
 Object obj = clazz.newInstance();
 Box b = (Box)obj;    // 必ず ClassCastException がスローされる


Box クラスは2つのクラスローダによって読み込まれています。クラスローダ CL がこのコードを含むクラスをロードしているとしましょう。このコードは MyClassLoader、Class、Object、Box の各クラスを参照しているため、 CL はこれらのクラスを読み込みます(他のクラスローダに委譲されていなければ)。このため、変数 b の型は CL によって読み込まれた Box クラスとなります。一方、myLoader もまた Box クラスを読み込んでいます。obj オブジェクトは myLoader によって読み込まれた Box クラスのインスタンスです。それゆえ、objオブジェクトの型と変数 b の型とではクラスが異なるため、上記コードの最終行では必ず ClassCastException が発生してしまうのです。

複数のクラスローダはツリー構造を形作っています。ブートストラップローダを除くそれぞれのクラスローダは、親クラスローダを持ちます。複数のクラスローダはツリー構造を形作っています。ブートストラップローダを除くそれぞれのクラスローダは、親クラスローダを持ち、通常はそれらが子のクラスローダを読み込みます。クラスのロード要求はクラスローダのツリー構造に従って親へと委譲されていきます。そのため、あるクラスローダにロードされたクラスに対してはロード要求する必要がなくなります。あるクラスローダがクラス C に対するロード要求を受け取ったとしても、それを実際に読み込むクラスローダは別のローダかもしれないのです。これらを区別するために、ここでは前者のローダを「C のイニシエータ」と予備、後者のローダを「Cのリアル・ローダ」と呼ぶことにします。

さらに、クラスローダ CL がクラス C の読み込みを要求された場合(ここでは CL が クラス C のイニシエータです)、親クラスローダである PL へ処理を委譲します。そして、クラスローダ CL はクラス C の中で参照しているどのクラスも決して読み込みません。CL はこれらのクラスのイニシエータにはなりません。そのかわり、親である PL がそれらのイニシエータとなってロード要求を処理するのです。クラス C の中で宣言されているクラスは C のリアル・ローダによって読み込まれます。

この振る舞いを理解するために、以下の例を考えてみましょう。

 public class Point {    // PL によってロードされる
 	private int x, y;
	public int getX() { return x; }
	:
 }
 public class Box {      // イニシエータは L であるが、リアル・ローダは PL である
	private Point upperLeft, size;
	public int getBaseX() { return upperLeft.x; }
	:
 } 
 public class Window {    // クラスローダ L によって読み込まれる
	private Box box;
	public int getBaseX() { return box.getBaseX(); }
 }

Window クラスはクラスローダ L によって読み込まれたとしましょう。Window のイニシエータもリアル・ローダも L です。Window クラスの中では Box クラスを参照しているため、JVM は L に Box クラスのロードを要求します。ここで、L がこの仕事を親である PL に委譲したとします。Box のイニシエータは L ですが、リアル・ローダは PL です。この場合、Point クラスの イニシエータは L ではなく Box のリアル・ローダである PL となります。つまり、L は Point クラスのロードを要求されないのです。

次に、少し違う例を考えてみましょう。

 public class Point {
     private int x, y;
     public int getX() { return x; }
         :
 }

 public class Box {      // イニシエータは L であるが リアル・ローダは PL である
     private Point upperLeft, size;
     public Point getSize() { return size; }
         :
 }

 public class Window {    // クラスローダLによって読み込まれる
     private Box box;
     public boolean widthIs(int w) {
         Point p = box.getSize();
         return w == p.getX();
     }
 }

今度は Window クラスの中で Point クラスも参照しています。この場合、クラスローダ L は Point クラスのロードを要求されると、それを PL に委譲しなければなりません。2つのクラスローダが同じクラスを重複して読み込むことは避けなければならないため、片方のクラスローダがもう片方へ処理を委譲します。

Point クラスのロードに際して L が PL に処理を委譲しなかった場合、widthIs() メソッドで ClassCastException が発生します。Box クラスのリアル・ローダが PL であるため、Box クラスの中で参照されている Point クラスもまた PL によって読み込まれています。widthIs() メソッド内で宣言された変数 p の型は Lによって読み込まれた Point クラスであるにもかかわらず、getSize() メソッドの戻り値は PL によって読み込まれた Point クラスのインスタンスとなっています。JVM はこれら2つの Point クラスを異なるものであるとみなすため、例外をスローするのです。

このような振る舞いは少々やっかいですが、重要です。もし、以下のようなステートメントが例外をスローしなかったら、Window クラスのプログラマはPoint オブジェクトのカプセル化を破壊することができてしまいます。

 Point p = box.getSize();

例えば、PL によって読み込まれた Point クラスの x フィールドはプライベート宣言されています。しかし、が以下のようにな Point クラスを読み込むと、Window クラスはフィールド x に直接アクセスできてしまうのです。

 public class Point {
     public int x, y;    // プライベート宣言ではない
     public int getX() { return x; }
         :
 }

Java のクラスローダに関してより詳しく理解したい場合には、以下の論文が役に立ちます。

Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine",
ACM OOPSLA'98, pp.36-44, 1998.

●3.3 javassist.Loader の使い方

Javassist は javassist.Loader という独自のクラスローダを提供しています。このクラスローダは、クラスファイルの読み込みに javassist.ClassPool を使用します。

たとえば、javassist.Loader は Javassist によって変更された特定のクラスをロードするのに役立ちます。

 import javassist.*;
 import test.Rectangle;

 public class Main {
   public static void main(String[] args) throws Throwable {
      ClassPool pool = ClassPool.getDefault();
      Loader cl = new Loader(pool);

      CtClass ct = pool.get("test.Rectangle");
      ct.setSuperclass(pool.get("test.Point"));

      Class c = cl.loadClass("test.Rectangle");
      Object rect = c.newInstance();
          :
   }
 }


このプログラムでは、test.Rectangle クラスを変更しています。test.Rectangle のスーパークラスを test.Point に変更しました。その後、変更されたクラスを読み込んで test.Rectangle の新たなインスタンスを生成しています。

もし、クラスがロードされるときに必要に応じて変更をしたいならば、javassist.Loader にイベントリスナを登録することで実現することができます。クラスローダがクラスをロードするとき、登録されたイベントリスナに通知されます。イベントリスナクラスは、以下のインターフェースを実装する必要があります。

 public interface Translator {
     public void start(ClassPool pool)
         throws NotFoundException, CannotCompileException;
     public void onLoad(ClassPool pool, String classname)
         throws NotFoundException, CannotCompileException;
 }


start() メソッドは、イベントリスナが javassist.Loader に addTranslator() メソッドによって登録されたときに呼び出されます。onLoad() メソッドは、javassist.Loader がクラスを読み込む前に呼び出されます。onLoad() メソッドは読み込まれたクラスを変更することができるのです。

例えば、以下のイベントリスナは全てのクラスを読み込む前にパブリックなクラスに変更します。

 public class MyTranslator implements Translator {
     void start(ClassPool pool)
         throws NotFoundException, CannotCompileException {}
     void onLoad(ClassPool pool, String classname)
         throws NotFoundException, CannotCompileException
     {
         CtClass cc = pool.get(classname);
         cc.setModifiers(Modifier.PUBLIC);
     }
 }

onLoad() メソッドは toBytecode() や writeFile() を呼び出す必要がないことに注意してください。javassist.Loader がクラスファイルを取得するために、これらのメソッドを呼び出すためです。

以下のサンプルコードでは、MyTranslator オブジェクトを組み込んだアプリケーション MyApp クラスを実行しています。

 import javassist.*;

 public class Main2 {
   public static void main(String[] args) throws Throwable {
      Translator t = new MyTranslator();
      ClassPool pool = ClassPool.getDefault();
      Loader cl = new Loader();
      cl.addTranslator(pool, t);
      cl.run("MyApp", args);
   }
 }

このプログラムを実行するには以下のようにします。

 % java Main2 arg1 arg2...

MyApp クラス及びそれ以外のアプリケーションクラスは、MyTranslator によって変換されます。

MyApp のようなアプリケーションクラスは、Main2、MyTranslator、ClassPool といったローダクラスにアクセスできないことに注意してください。これらは異なるクラスローダによって読み込まれたものだからです。Main2 のようなローダクラスはデフォルトの Java クラスローダによって読み込まれますが、アプリケーションは javassist.Loader によって読み込まれます。

javassist.Loader は java.lang.ClassLoader とは違う順序でクラスを検索します。ClassLoader はまず親に対してクラスの読み込みを委譲し、親ローダがクラスを見つけられなかった場合のみ、自分でクラスを検索します。一方、javassist.Loader は親へ委譲する前に自分でクラスを検索します。親への委譲を行うのは、以下のような場合です。

-ClassPool に対する get() メソッドの呼び出しでクラスが見つからなかった場合。
-delegateLoadingOf() メソッドによって、親クラスローダでクラスを読み込むように指定されている場合。

このような順番のため、Javassist は変更されたクラスを読み込むことができます。しかし、何らかの理由で更新されたクラスの読み込みに失敗した場合は親クラスローダへ処理を委譲します。一度親クラスローダによってクラスがロードされると、そのクラスで参照されている他のクラスも親クラスローダで読み込まれ、それらは変更することができなくなります。あるクラス C の中で参照される全てのクラスは、C のリアル・ローダに読み込まれるということをもう一度思い出してください。もしあなたのプログラムが変更されたクラスの読み込みに失敗したら、そのクラスを利用している全てのクラスが javassist.Loader によって読み込まれたものであるかどうかを確認する必要があります。

●3.4 クラスローダの作成

Javassist を利用したシンプルなクラスローダは、以下のようなものです。

 import javassist.*; 

 public class SampleLoader extends ClassLoader {
     /* Call MyApp.main().
      */
     public static void main(String[] args) throws Throwable {
         SampleLoader s = new SampleLoader();
         Class c = s.loadClass("MyApp");
         c.getDeclaredMethod("main", new Class[] { String[].class })
          .invoke(null, new Object[] { args });
     }

     private ClassPool pool;

     public SampleLoader() throws NotFoundException {
         pool = new ClassPool();
         pool.insertClassPath("./class"); // MyApp.class must be there.
     }

     /* Finds a specified class.
      * The bytecode for that class can be modified.
      */
     protected Class findClass(String name) throws ClassNotFoundException {
         try {
             CtClass cc = pool.get(name);
             // modify the CtClass object here
             byte[] b = cc.toBytecode();
             return defineClass(name, b, 0, b.length);
         } catch (NotFoundException e) {
             throw new ClassNotFoundException();
         } catch (IOException e) {
             throw new ClassNotFoundException();
         } catch (CannotCompileException e) {
             throw new ClassNotFoundException();
         }
     }
 }

MyApp クラスはアプリケーションプログラムです。このプログラムを実行するには、まず最初に ./class ディレクトリ以下にクラスファイルを配置しておきます。このディレクトリはクラス検索パスに含まれないようにしてください。MyApp.class は SampleLoader の親ローダであるデフォルトシステムクラスローダによって読み込まれます。./class というディレクトリ名はコンストラクタ内で insertClassPath() メソッドによって指定されています。./class の代わりに好きなディレクトリ名に変更することもできます。その後、以下のようにコマンドラインから実行します。

 % java SampleLoader

クラスローダは MyApp クラス(./class/MyApp.class)を読み込み、その main() メソッドにコマンドラインパラメータを渡して呼び出します。

これが、Javassist を利用するためのもっとも簡単な方法です。しかし、より高度なクラスローダを作成するには Java のクラスローディングメカニズムに対するより深い知識が必要となります。たとえば、ここで示したプログラムでは MyApp クラスが SampleLoader とは異なるクラスローダによって読み込まれたため、SampleLoader とは別の名前空間に存在しています。そのため、MyAppクラスは SampleLoader クラスに対して直接アクセスすることができません。

●3.5 システムクラスの変更

java.lang.String のようなシステムクラスは、システムクラスローダ以外のクラスローダで読み込むことはできません。そのため、さきほど示した SampleLoader や javassist.Loader はシステムクラスをロード時に変更することができません。

もしそのようなことを実現したい場合、システムクラスを静的に変更しなければなりません。以下のプログラムでは、java.lang.String に新しく hiddenValue というフィールドを追加しています。

 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.get("java.lang.String");
 cc.addField(new CtField(CtClass.intType, "hiddenValue", cc));
 pool.writeFile("java.lang.String", ".");

このプログラムは "./java/lang/String.class" というファイルを生成します。

変更された String クラスを使用して MyApp を実行するには、以下のようにします。

 % java -Xbootclasspath/p:. MyApp arg1 arg2...

MyApp が以下のようになっているとしましょう。

 public class MyApp {
     public static void main(String[] args) throws Exception {
         System.out.println(String.class.getField("hiddenValue").getName());
     }
 }

更新された String クラスが正しく読み込まれていれば、MyApp は hiddenValue と表示します。

:注意:rt.jar に含まれるシステムクラスをオーバーライドするためにこのテクニックを使うアプリケーションは、the Java 2 Runtime Environment のバイナリコードライセンスに違反するため、実際に使うべきではありません。


1 2 3 4 5

ページトップへ
Java? は、米国Sun Microsystems,Inc. の登録商標です。
原著:Copyright c 2000-2005 O’Reilly Media, Inc. All rights reserved.
邦訳:Copyright c 2005 Acroquest, Co.,Ltd. All rights reserved.