Seaserのホットデプロイは開発向けにはなかなか便利ですが、これを使えるようにフレームワークの構成を考えるのはかなりしんどいものがあります。今回はハマったポイントとしてリフレクションを取り上げます。
Seaserのホットデプロイ
Seaserのホットデプロイは
実行時に特定箇所でクラスローダを毎回新しくするというシンプルなものです。
結構、割り切った作りになっているなぁというのが感想ですが、そのシンプルさと原理からいろいろと制限事項も生まれてしまう。
今回、私がハマったところはフレームワーク内でリフレクションをする際に、クラスローダの違いからClassCastExceptionを出したり、NoSuchMethodExceptionを出したりしたことでした。
クラスローダの基礎知識
まず、基礎知識としてクラスローダが違う場合、同じClassでも代入互換性がありません。Classの同一性は、Class自身とそのクラスローダによって定められます。
次に、クラスローダは階層構造をなしていて、まず親クラスローダにロード処理が移譲されます。
ClassLoader
ClassLoader クラスは、委譲モデルを使ってクラスとリソースを探します。ClassLoader の各インスタンスは、関連する親クラスローダーを持ちます。クラスまたはリソースを見つけるために呼び出されると、ClassLoader インスタンスはそれ自体でクラスまたはリソースの検索を試みる前に、その検索を親クラスに委譲します。「ブートストラップクラスローダー」と呼ばれる仮想マシンの組み込みクラスローダーはそれ自体では親を持たず、ClassLoader インスタンスの親として動作します。
このため、親が同じである場合は、同じクラスローダ(つまるところ親)で読み込まれることで代入互換を持つことがあります。
クラスローダを跨ぐ場合のClassCastException
Seaserのホットデプロイは
HotdeployUtilのstart()とstop()の間でクラスローダが変わることで実現しています。
なので、この区間の中と外ではクラスローダが異なります。そのため、戻り値のClassCastExceptionが発生することがあります。
SingletonS2ContainerFactory.init();
S2Container container = SingletonS2ContainerFactory.getContainer();
HotdeployUtil.start();
Object o = container.getComponent("testLogic");
System.out.println(o.getClass().getClassLoader());
Hoge hoge = (Hoge)o;
HotdeployUtil.stop();
log4j:WARN No appenders could be found for logger (org.seasar.framework.container.factory.S2ContainerFactory).
log4j:WARN Please initialize the log4j system properly.
org.seasar.framework.container.hotdeploy.HotdeployClassLoader@6e70c7
Exception in thread "main" java.lang.ClassCastException: test.logic.impl.TestLogicImpl cannot be cast to test.Hoge
at test.LoaderTest.main(LoaderTest.java:19)
こうした再読込をおこなうタイプのクラスローダは親クラスローダへの委譲を行わないことで実現できます。親へ委譲してしまうと読み込み済みのクラスをリフレッシュすることができません。
しかし、この場合、クラスローダ越境ができなくなります。通常は、親への委譲を先に行うことで、共通して読み込まれるクラスには互換があったわけですが、クラスをロードしなおすために、親への委譲を行わないようにすると、上記例のようにClassCastExceptionとなってしまうのです。
リフレクションの際のメソッドシグネチャにも注意!
こうしたクラスローダを跨いだ箇所でリフレクションを行っていると、わかりにくいバグに遭遇することがあります。
SingletonS2ContainerFactory.init();
S2Container container = SingletonS2ContainerFactory.getContainer();
HotdeployUtil.start();
Object o = container.getComponent("testLogic");
Method[] ms = o.getClass().getMethods();
System.out.println(Arrays.toString(ms));
Method m = o.getClass().getMethod("setHoge", Hoge.class);
HotdeployUtil.stop();
log4j:WARN No appenders could be found for logger (org.seasar.framework.container.factory.S2ContainerFactory).
log4j:WARN Please initialize the log4j system properly.
[public void test.logic.impl.TestLogicImpl.setHoge(test.Hoge), public test.Hoge test.logic.impl.TestLogicImpl.getHoge(), public native int java.lang.Object.hashCode(), public final native java.lang.Class java.lang.Object.getClass(), public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException, public final void java.lang.Object.wait() throws java.lang.InterruptedException, public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException, public boolean java.lang.Object.equals(java.lang.Object), public java.lang.String java.lang.Object.toString(), public final native void java.lang.Object.notify(), public final native void java.lang.Object.notifyAll()]
Exception in thread "main" java.lang.NoSuchMethodException: test.logic.impl.TestLogicImpl.setHoge(test.Hoge)
at java.lang.Class.getMethod(Unknown Source)
at test.LoaderTest.main(LoaderTest.java:24)
ご覧のように、setHoge(Hoge)が存在しているにも関わらず、NoSuchMethodExceptionが投げられsetHoge(Hoge)はない!と言われてしまうのです。これは、getMethod()の引数に渡すHoge.classと、読み込まれたコンポーネントのHoge.classでクラスローダが異なるためです。
開発用と割り切って全体を囲ってしまう
クラスローダの越境は非常に厄介ですので、処理の頭からお尻までまるごとクラスローダを差し替えるように使うのが無難です。
Servletであれば、ホットデプロイ用の
HotdeployFilter
が用意されていますのでそちらでリクエストの全体で差し替えるほうがよいでしょうね。
投稿日時 : 2008年6月17日 15:39