HOT deployでのClassCastException対策

Seasar2.4.xのHOT deployでは、filterやutil、entityといったのようなSMART deploy対象外のクラスから、actionやservice、dao、dtoといったSMART deploy対象のクラスを参照すると、ClassCastExceptionが発生する。この対策を考えてみた。
ちなみに、COOL deployでは発生しない。

2009/4/14 追記
utilやentityがSMART deploy対象外クラスと書いたが、S2Containerにコンポーネントの自動登録がされない(おそらくS2Containerがインスタンスを管理する必要がないから)だけで、コードの変更はHotに反映されるし、クラスローダーもちゃんとHotdeployClassLoaderになる。なので、SMART deploy対象外というのは誤りと思いますので、訂正しました。

原因

なぜ、HOT deployでは、SMART deploy対象外のクラスからSMART deploy対象のクラスを参照すると、ClassCastExceptionが発生するのか?例えば、SMART deploy対象外のクラスで次のような記述があると、ClassCastExceptionが発生する。

XxxDto xxxDto = (XxxDto) container.getComponent("xxxDto");

上記の場合、S2Containerから取得されるXxxDtoは、dtoがSMART deploy対象クラスであり、HOT deploy対象クラスとなるため、org.seasar.framework.container.hotdeploy.HotdeployClassLoaderでロードされたXxxDtoとなる。一方、キャスト先のXxxDtoは、この記述を実装しているクラスがSMART deploy対象外であるため、オリジナルのクラスローダー(Tomcatならorg.apache.catalina.loader.WebappClassLoader)にロードされたXxxDtoとなる。Javaでは、同一クラスであってもロードしたクラスローダーが異なれば、別クラスとして認識されるため、ClassCastExceptionが発生してしまう。
ちなみに、これがWebアプリであれば、一度ClassCastExceptionが発生すると、次回のリクエスト以降は発生しなくなる。これは、XxxDtoがClassCastException発生直前にオリジナルクラスローダーにロード済みのため、次回以降のリクエスト時には、例えSMART deploy対象クラスであってもHotDeployClassLoaderがXxxDtoをロードしないようになっており、S2Containerから取得されるXxxDtoもオリジナルクラスローダーにロードされたXxxDtoとなるため。

対策

原因を見れば分かる通り、同一のクラスローダーにロードされるようにすればClassCastExceptionは防げる。
その方法をいくつか考えてみる。

案1.SMART deploy対象外クラスを対象にする。

全部HotDeployClassLoaderにロードさせるようにできればClassCastExceptionは起きないという発想だけど、Creator、Customizerを作成して、S2Containerにコンポーネント登録させるところはできたとしても、HOT deployに対応させるのは、おそらく相当敷居が高いと思う。

案2.SMART deploy対象クラスとSMART deploy対象外クラスを明確に分離して、対象外クラスから対象クラスを参照しないようにする。

対象クラスと対象外クラスでパッケージを分けてしまう。
http://d.hatena.ne.jp/higayasuo/20090317/1237268663

http://ml.seasar.org/archives/seasar-user/2009-February/016916.html
で、示されている方法。これが公式な対策ということになるのかな?

ただ、個人的にはどうもこの方法はしっくりこない。
例えば、Filterでアクセスロギングを行っていて、ログインユーザー情報も併せてロギングするために、FilterでHttpSession上のログインユーザーDtoを取得しているようなアプリの場合、パッケージ分割の考え方にならうと、ログインユーザーDtoはSMART deploy対象外パッケージに置くことになる。開発体制として、Filter等の共通処理部分は共通基盤チームみたいなのが開発して、個々の業務画面等を個々の開発者が開発するような体制(SIerではよくある体制だと思う)を想定した場合、ログインユーザーDtoなんかは、色んな画面で参照するものなので、個々の開発者が皆、パッケージの違いを意識しないといけなくなってしまう。個々の開発者はSMART deploy対象クラスからログインユーザーDtoを参照するので元々ClassCastExceptionが発生しないにも関わらず、だ。個々の開発者も皆、HOT deployのクラスローダー云々とかClassCastException云々を知るべきでその上でパッケージ分割理由も皆で共有する、というような方針であれば、これでいいと思うけど、個々の開発者にそこまで意識させない方針であれば、パッケージ分割がどうしても非直観的になってしまわないだろうか?という気がする。

2009/3/19 追記
コメントで指摘いただいた。パッケージの違いはIDEが吸収してくれる(自力でパッケージを書くわけではない)から個々の開発者が意識する必要はないと思う。

案3.diconに書く。

SMART deploy対象外クラスから参照されるSMART deploy対象クラスをdiconに書くことで、SMART deploy対象外にする方法。Seasar2.3のコンポーネント自動登録を使うのも同様。
デメリット?は、SMART deploy対象外になるので、customizer.diconで定義したInterceptorとかが適用されなくなること。まあ、S2Containerの管理対象からはずれるわけではないので、Seasar2.3の記述方法で記述すれば、AOPも仕掛けられるけど。

案4.HotDeployClassLoaderより先にオリジナルクラスローダーにロードさせる。

上述の通り、HotDeployClassLoaderはオリジナルクラスローダーにロード済みのクラスはロードしないので、SMART deploy対象外クラスから参照されるSMART deploy対象クラスを、HotDeployClassLoaderより先にオリジナルクラスローダーにロードさせることで、オリジナルクラスローダーのみを使用することとなり、ClassCastExceptionを防ぐ。
HOT deploy対象外だけど、SMART deploy対象ということになる。なので、案3.のようにcustomizer.diconの定義内容が適用されないなんてこともない。
やり方は、Webアプリであれば、例えば次のようなFilterを作成し、web.xmlで、HotdeployFilterよりも前に適用されるように定義すればよい。HOT deployのためだけの処理が運用時に入っているのが気に入らなければ、COOL deploy時は、web.xmlコメントアウトすることで処理をはずすこともできる(案3.でもdiconから記述を削除すれば同じことになりそうだけど、それによってcustomizer.diconの適用がされることになったり等、影響が出るのでこちらの方が安心な気がする)

XxxDtoがSMART deploy対象外クラスから参照される場合の例

public class ExcludeHotdeployFilter implements Filter {

    private FilterConfig filterConfig;

    public void init(FilterConfig filterConfig) {
        this.filterConfig = filterConfig;
    }

    public void destroy() {
        this.filterConfig = null;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        Class clazz = XxxDto.class;
        chain.doFilter(req, res);
    }
}

例では、XxxDtoを直書きしているけど、Filterなのでパラメータを使って、Class.forName()でやれば、web.xmlにクラス名を記述することも可能。


とりあえず、Webアプリであれば、案4.が簡単でデメリットも特になくてよいと思う。
ただし、あまりないとは思うけど、SMART deploy対象外クラスから参照するSMART deploy対象クラスが大量にある場合は、案3.とか案4.もしんどそうなので、案2ということになるかなあ。