wicketがホットデプロイを実現する方法

はじめに

 Wicketホットデプロイの実装方法を調べていたら、なかなか面白かったのでメモをすることに。
 クラスローダをReloadingClassLoaderに差し替えるところからすべては始まるのですが、差し替え方は今回は取り扱いません。

解析を実施した環境

ホットデプロイの実現方法の概要

 ReloadingClassLoaderと、ModificationWatcherがホットデプロイ実現に必要な主要構成要素となります。下記のクラス図もどきに構成要素の関係を示しております。



 クラスローダをReloadingClassLoaderに差し替えることにより下記のことを実現しています。

  • 監視対象クラスのピックアップ
  • リローディング対象クラスオブジェクトのみの破棄

 ModificationWatcherをスレッドとして起動し下記のことを実現しています。
 厳密にはModificationWatcherの内部クラスにスレッドとして実行されるコードは記述されているのですが、それはこの場ではおいておきます。

  • 監視に必要な情報をEntryクラスのインスタンスして保持し、クラスファイルを常時更新チェック
  • 変更検出時にクラスオブジェクト破棄

監視対象クラスのピックアップ

 ピックアップはクラスファイル読み込み時に行われます。クラスオブジェクトが必要となった場合ReloadingClassLoader.loadClassメソッドが呼び出されます。このメソッド内部では下図の処理が実行されます。

ポイントは以下の3点です。

  • リローディング対象はReloadingClassLoaderが読み込む。
  • リローディング対象は、クラスファイルを発見できれば、監視対象としてModificationWatcherに登録する。
  • リローディング対象以外は親クラスローダが読み込む。

監視と変更検出の方法

監視方法

 監視はWicketが起動した監視用スレッドによって行われます。下図は監視用スレッドを止めたところです。赤枠のスレッド名をみますと、それらしい名前が付いていることが確認できると思います。

変更が見つかったとき

 ReloadingClassLoaderのインスタンスを再生成します。
 再生成するためのコードが、ReloadingClassLoader.initメソッド内に匿名クラスとして記述されているコードです。
 以下に抜粋します。

public void init(final FilterConfig filterConfig) throws ServletException
{
    reloadingClassLoader.setListener(new IChangeListener()
    {
        public void onChange()
        {
            // Remove the ModificationWatcher from the current reloading class loader
            reloadingClassLoader.destroy();

            /*
             * Create a new classloader, as there is no way to clear a ClassLoader's cache. This
             * supposes that we don't share objects across application instances, this is almost
             * true, except for Wicket's Session object.
             */
            reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader());
            try
            {
                init(filterConfig);
            }
            catch (ServletException e)
            {
                throw new RuntimeException(e);
            }
        }
    });

    super.init(filterConfig);
}

 getClass().getClassLoader()は親クラスローダを取得しています。TomcatならばWebappClassLoaderのインスタンスが取れる、はず。
 ReloadingClassLoaderを破棄することにより、以下の二つも破棄します。

  • 監視対象クラスのリスト
  • ReloadingClassLoaderが既に読み込んだクラスオブジェクト

 ReloadingClassLoaderを一度破棄することにより、既に読み込んだクラスを「忘れる」ことになります。その結果、「監視対象クラスのピックアップ」の章のアクティビティ図に記載したfindLoadedClassメソッドが、一度読み込んだクラスについても「まだロードしていない」という応答を返すようになるわけです。

まとめ

 以下に解析して分かったWicketが採用している方式の良い所、いまいちな所をまとめます。

良い所
  • 既に読み込んだクラスファイルに変更があった場合のみ、再読み込みが実施される。まだ読み込んでいないクラスファイルに変更があった場合には何もしない。
いまいちな所

  • 変更が無かったクラスファイルも読み直すことになる。
  • クラスローダが変更されてしまう。
 このイマイチなところが実は案外厄介だったりします。
 上に「ReloadingClassLoaderが既に読み込んだクラスオブジェクトを破棄する」と記述しましたが、これは概念的な話です。Javaでオブジェクトが破棄されるためには、参照がすべてなくなる必要があります。
 で、ReloadingClassLoader以外にもクラスオブジェクトへの参照を持つものが存在します。それは、既に生成してしまったオブジェクトです。
 これがどう厄介なのかは、今回は省略します。

本当は

 もっと細かいことも書きたかったのですが、うまくまとめられそうに無かったので、この辺で……。