はじめに
Javaのクラスローディングフレームワークは、強力かつ柔軟です。このクラスローディングフレームワークを使えば、アプリケーションがクラスライブラリにアクセスする際に、静的な「インクルード」ファイルにリンクする必要がなくなります。その代わりに、指定の場所(例えばCLASSPATH環境変数で定義したディレクトリやネットワークロケーションなど)から、ライブラリクラスを格納したアーカイブファイルとリソースがロードされます。このシステムにより、実行時のクラスとリソースへの参照が動的に解決されるため、更新と改訂バージョンのリリース作業が簡略化されます。それでも、各ライブラリは独自の依存関係をひととおり持っており、アプリケーションが正しいバージョンを確実にうまく参照できるかどうかは、開発者と導入担当者次第です。あいにく、既定のクラスローディングシステムと特定の依存関係はバグやシステムクラッシュ、あるいはもっと悪い事態を引き起こす可能性があります(というよりも実際に引き起こしています)。
本稿では、このような問題を解決するためのクラスローディングコンテナフレームワークを紹介します。
Javaのクラスパス
Javaでは、ランタイムがクラスや他のリソースを必要に応じて検索する際のパスを指定するのに環境プロパティ/変数、それにCLASSPATHを使用します。CLASSPATHを適切に設定するには、CLASSPATH環境変数を設定するか、Javaのコマンドラインオプションである-classpathを使います。
一般に、Javaのランタイムは次の順序でクラスを探し、ロードします。
- ブートストラップクラスのリストに記載されているクラス
- 拡張クラスのリストに記載されているクラス
- ユーザークラス
アーカイブとクラスパス
.jarや.zipなどのアーカイブファイルには、アーキテクチャに関する情報を提供したり、アーカイブのプロパティを設定したりするための「マニフェストファイル」を格納することができます。このマニフェストファイルでも、アーカイブのリストとディレクトリを格納するClass-Pathという名前のエントリを含めることによってクラスパスを拡張することができます。JDK 1.3では、必要に応じてオプションのjarやディレクトリを指定するエントリ用としてClass-Pathマニフェストが導入されました。Class-Pathエントリの例を次に示します。
Class-Path: mystuff/utils.jar mystuff/logging.jar mylib/
Javaでは、クラスをロードするための場所やファイルのリストを指定できる、拡張可能なモデルが用意されています。しかし、「そのクラスパスに存在するライブラリが、実行クラスが期待しているものとはバージョンが違う」という問題が発生する可能性があります。
クラスパスバージョンの競合
JavaのランタイムIDは完全修飾名(クラス名の前にパッケージ名を付加したもの)で定義され、それらはすべて、そのクラスをロードしたクラスローダのIDの後ろに付加されます。従って、複数のクラスローダによってロードされたインスタンスは、Javaランタイムからは別々のエンティティと見なされます。つまり、ランタイムは同じクラスのいくつかのバージョンを任意のタイミングでロードできるということです。これは非常に強力で柔軟な機能ですが、賢く使わなければ、その副作用に悩まされる可能性があります。
例えば、同じようなセマンティクスを持つ複数のソース(例えばファイルシステムとデータベース)のデータにアクセスするエンタープライズアプリケーションを開発している場面を想像してください。この種のアプリケーションは多くの場合、類似のデータソースを抽象化するDAO(Data Access Object)を使ってデータアクセス層を公開するという方法を採用しています。さらに、DAOクライアントの新機能についての要望に応えるために、APIをわずかに変更した新しいバージョンのデータベースDAOをロードしたとします。ただし、まだ新しいAPIに対応できていない他のクライアントのために、古いDAOも残しておく必要があります。一般的なランタイム環境では、古いDAOが新しいバージョンのDAOで単純に置き換えられてしまい、新しいインスタンスはすべて新しいバージョンから作成されることになります。しかし、ランタイム環境を止めないままで更新を行った場合(ホットローディング)は、古いDAOに基づく既存のインスタンスと、新しいDAOから作成されたインスタンスがメモリ内に共存することになります。この点はどう考えても混乱の元です。さらに困るのは、あるDAOクライアントが、古いバージョンのDAOのインスタンスが作成されることを期待しているにもかかわらず、実際に取得したのはAPI変更後の新しいバージョンのインスタンスだった、という危険が生じることです。このように、いろいろと厄介な問題が生じる可能性があります。
安定性と安全性を確保するために、呼び出し側のコードは、使用したいクラスの正確なバージョンを「指名」できる必要があります。この問題に対処するには、クラスローディングメカニズムとコンポーネントコンテナモデルを作成し、いくつかのシンプルなクラスローディングテクニックを使用します。
アーカイブとコンポーネント
アーカイブファイル(jarファイル、zipファイルなど)は、Javaのクラスローディングメカニズムや開発ツールと密接に結び付いているため、自己定義コンポーネントの「入れ物」として利用するのにちょうどよい候補となります。Javaコンポーネントをアーカイブの中にパッケージ化してデプロイするという処理がうまくいっているのは、次の条件が成立しているからです。
- インスタンス化するコンポーネントのバージョンを開発者が明確に指定できる。
- コンポーネントの補助クラスの適切なバージョンを、コンポーネントと同じjarファイル内の情報に基づいて正しくロードできる。
これにより、どのバージョンのコンポーネントを実際に作成して使用するかをコンポーネントの開発者と使用者が完全に制御できます。
以降では、コンポーネントとコンポーネントの名前空間を、どのアーカイブに格納するかによって定義するという考え方について説明します。
補助リソースの共有
標準クロスローダを使ってJavaの共有ライブラリを扱う場合の最大の問題は、すべてのクラスが単一の名前空間にロードされてしまうことです。そのせいで、同じライブラリの別々のバージョンを任意のタイミングで使い分けるのは、非常に困難です。コンポーネントや補助ライブラリのロード先となる独自の名前空間を、開発者が自分で定義できる必要があります。
JavaのクラスのランタイムIDはクラスの完全修飾名とクラスローダのIDによって定義されるので、個々のクラスローダには既に名前空間があります。従って、そのクラスローダを利用して、コンポーネント(およびその依存コンポーネント)の名前空間を定義するコンポーネントコンテナを作成することができます。
例えば、「com.jeffhanson.components.HelloWorld」という名前のクラスがあり、このクラスを2種類のバージョンで動かしたいとします。この場合の解決策は、それぞれのバージョンを別々のクラスローダで作成することです。この概念を図1に示します。
後で例を紹介しますが、1つのクラスを2つの異なるクラスローダでインスタンス化するというテクニックでは、実際には1つの仮想名前空間が作成されます。ただし本稿の例では、同じバージョンのクラスのインスタンスを複数作成しただけです。
同じクラスの複数のバージョンをロードしてインスタンス化する処理を容易にするために、以降では、クラスローダの名前空間メカニズムに基づいて同じクラスの別々のバージョンをロードできるようにしたコンポーネントコンテナフレームワークの例を紹介します。