Javaのクラスローディングを制御するはじめにJavaのクラスローディングフレームワークは、強力かつ柔軟です。このクラスローディングフレームワークを使えば、アプリケーションがクラスライブラリにアクセスする際に、静的な「インクルード」ファイルにリンクする必要がなくなります。その代わりに、指定の場所(例えばCLASSPATH環境変数で定義したディレクトリやネットワークロケーションなど)から、ライブラリクラスを格納したアーカイブファイルとリソースがロードされます。このシステムにより、実行時のクラスとリソースへの参照が動的に解決されるため、更新と改訂バージョンのリリース作業が簡略化されます。それでも、各ライブラリは独自の依存関係をひととおり持っており、アプリケーションが正しいバージョンを確実にうまく参照できるかどうかは、開発者と導入担当者次第です。あいにく、既定のクラスローディングシステムと特定の依存関係はバグやシステムクラッシュ、あるいはもっと悪い事態を引き起こす可能性があります(というよりも実際に引き起こしています)。 本稿では、このような問題を解決するためのクラスローディングコンテナフレームワークを紹介します。 JavaのクラスパスJavaでは、ランタイムがクラスや他のリソースを必要に応じて検索する際のパスを指定するのに環境プロパティ/変数、それにCLASSPATHを使用します。CLASSPATHを適切に設定するには、CLASSPATH環境変数を設定するか、Javaのコマンドラインオプションである-classpathを使います。 一般に、Javaのランタイムは次の順序でクラスを探し、ロードします。
これらは、例えば「rt.jar」内のクラスのように、Javaプラットフォームを統合するクラスです。
これらのクラスではJava、プラットフォームを拡張するExtension Mechanism Frameworkというフレームワークを、.jarや.zipなど、/lib/extランタイム環境ディレクトリにあるアーカイブファイルと共に使用します。
これらのクラスでは、-classpathコマンドラインオプションやCLASSPATH環境変数で識別される拡張メカニズムアーキテクチャを使いません。
アーカイブとクラスパス.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コンポーネントをアーカイブの中にパッケージ化してデプロイするという処理がうまくいっているのは、次の条件が成立しているからです。
これにより、どのバージョンのコンポーネントを実際に作成して使用するかをコンポーネントの開発者と使用者が完全に制御できます。 以降では、コンポーネントとコンポーネントの名前空間を、どのアーカイブに格納するかによって定義するという考え方について説明します。 補助リソースの共有標準クロスローダを使ってJavaの共有ライブラリを扱う場合の最大の問題は、すべてのクラスが単一の名前空間にロードされてしまうことです。そのせいで、同じライブラリの別々のバージョンを任意のタイミングで使い分けるのは、非常に困難です。コンポーネントや補助ライブラリのロード先となる独自の名前空間を、開発者が自分で定義できる必要があります。 JavaのクラスのランタイムIDはクラスの完全修飾名とクラスローダのIDによって定義されるので、個々のクラスローダには既に名前空間があります。従って、そのクラスローダを利用して、コンポーネント(およびその依存コンポーネント)の名前空間を定義するコンポーネントコンテナを作成することができます。 例えば、「com.jeffhanson.components.HelloWorld」という名前のクラスがあり、このクラスを2種類のバージョンで動かしたいとします。この場合の解決策は、それぞれのバージョンを別々のクラスローダで作成することです。この概念を図1に示します。 図1 複数のクロスローダの使用:Javaの命名規則の仕組みにより、別々のクロスローダを使うと別々の名前空間が定義される ![]() 後で例を紹介しますが、1つのクラスを2つの異なるクラスローダでインスタンス化するというテクニックでは、実際には1つの仮想名前空間が作成されます。ただし本稿の例では、同じバージョンのクラスのインスタンスを複数作成しただけです。 同じクラスの複数のバージョンをロードしてインスタンス化する処理を容易にするために、以降では、クラスローダの名前空間メカニズムに基づいて同じクラスの別々のバージョンをロードできるようにしたコンポーネントコンテナフレームワークの例を紹介します。 クラスローダの名前空間を利用するコンポーネントコンテナフレームワークという概念は、jarまたはzipアーカイブで定義されたコンポーネントとそれらのコンポーネントが必要とする補助クラスをロードする役割を持ったコンテナエンティティとして実装できます。このフレームワークの目的は次のとおりです。
コンポーネントとそれに関連する補助ファイルを定義するには、次のような設定ファイルが必要です。 <?xml version="1.0"?> <component name= "com.jeffhanson.components.HelloWorld"> <component-archive> HelloWorldComponentV1.jar </component-archive> <ancillary-resources> <ancillary-resource> log4j-1.2.12.jar </ancillary-resource> <ancillary-resource> concurrent-1.3.4.jar </ancillary-resource> </ancillary-resources> </component> 設定ファイルをもう1つ紹介します。上の例と、次の例の要素を比べてみてください。変更されている唯一の点は、 <?xml version="1.0"?> <component name= "com.jeffhanson.components.HelloWorld"> <component-archive> HelloWorldComponentV2.jar </component-archive> <ancillary-resources> <ancillary-resource> log4j-1.2.12.jar </ancillary-resource> <ancillary-resource> concurrent-1.3.4.jar </ancillary-resource> </ancillary-resources> </component> フレームワークがクラスをロードする際に、指定した場所からのみロードするよう保証するためには、 次のコードは、この制限付きクラスローダのメカニズムを示しています。 package com.jeffhanson.components; import java.net.URL; import java.net.URLClassLoader; public class RestrictedURLClassLoader extends URLClassLoader { public RestrictedURLClassLoader( URL[] urls) { super(urls, null); } public Class loadClass(String name) throws ClassNotFoundException { Class cls = super.loadClass(name); if (cls == null) { throw new ClassNotFoundException( "Restricted ClassLoader" + " is unable to find class: " + name); } return cls; } } この制限付きクラスローダは、コンポーネントコンテナがコンポーネントや補助クラスをロードする際に使われます。 コンポーネントコンテナは、現在のスレッドのコンテキストクラスローダを利用して、目的のコンポーネントのURLを見つけます。その後、このURLを制限付きクラスローダに与え、コンポーネントのインスタンス作成に使用します。さらに、このコンポーネントクラスを以後の呼び出しに備えてキャッシュします。リスト1にコンポーネントコンテナのコードを示し、図2にコンポーネントコンテナフレームワークの各クラス間の関係を示します。 リスト1 コンポーネントコンテナクラスのコード
package com.jeffhanson.components; import org.apache.commons.configuration. ConfigurationException; import org.apache.commons.configuration. XMLConfiguration; import org.apache.commons.configuration.Configuration; import org.apache.log4j.Logger; import java.net.URL; import java.util.*; import java.io.IOException; public class ComponentContainer { // =============================================== // static fields // =============================================== private static Logger log = Logger.getLogger(ComponentContainer.class); // =============================================== // member fields // =============================================== private String componentArchive = ""; private URL[] ancillaryClassPathURLs = null; private HashMap componentCache = new HashMap(); // =============================================== // constructors // =============================================== /** * Constructs a new ComponentContainer for * components named by the specified component * names. The names are assumed to * refer to JAR/zip files which will be downloaded * and opened as needed. * * @param componentConfigFileName the name of the * configuration * file for the * component from * which to * load classes * and resources */ public ComponentContainer(String componentConfigFileName) throws ConfigurationException { XMLConfiguration config = new XMLConfiguration( componentConfigFileName); this.componentArchive = config.getString("component-archive"); Configuration subConfig = config.subset("ancillary-resources"); if (subConfig != null) { String[] resNameArr = subConfig.getStringArray( "ancillary-resource"); if (resNameArr != null && resNameArr.length > 0) { this.ancillaryClassPathURLs = new URL[resNameArr.length]; for (int i = 0; i < resNameArr.length; i++) { this.ancillaryClassPathURLs[i] = Thread.currentThread(). getContextClassLoader(). getResource(resNameArr[i]); } } } } /** * Constructs a new ComponentContainer for * components named by the specified component * names. The names are assumed to refer to JAR/zip * files which will be downloaded and opened * as needed. * * @param componentArchive the name of the * archive from which * to load the component, * classes and resources */ public ComponentContainer(String componentArchive, URL[] ancillaryClassPathURLs) { this.componentArchive = componentArchive; if (ancillaryClassPathURLs != null && ancillaryClassPathURLs.length > 0) { this.ancillaryClassPathURLs = new URL[ancillaryClassPathURLs.length]; System.arraycopy(ancillaryClassPathURLs, 0, this.ancillaryClassPathURLs, 0, ancillaryClassPathURLs.length); } } // ============================================== // non-public methods // ============================================== protected Class findLoadedComponent(String name) { if (componentCache.get(name) != null) { return (Class)componentCache.get(name); } return null; } /** * Finds a component by name. This method will be * invoked by the loadComponent method. * * @param name The name of the component * @return The component’s <tt>Class</tt> object * @throws ComponentNotFoundException If the * component * could not * be found */ protected Class findComponent(String name) throws ComponentNotFoundException { ClassLoader ctxClsLoader = Thread.currentThread(). getContextClassLoader(); URL url = ctxClsLoader.getResource( componentArchive); if (url != null) { URL[] urls = null; if (ancillaryClassPathURLs != null && ancillaryClassPathURLs.length > 0) { urls = new URL[ ancillaryClassPathURLs.length + 1]; System.arraycopy(ancillaryClassPathURLs, 0, urls, 0, ancillaryClassPathURLs.length); urls[ancillaryClassPathURLs.length] = url; } else { urls = new URL[1]; urls[0] = url; } RestrictedURLClassLoader urlClassLoader = new RestrictedURLClassLoader(urls); try { Class cls = urlClassLoader.loadClass( name); if (cls != null) { return cls; } } catch (ClassNotFoundException e) { // ignore } } throw new ComponentNotFoundException(name); } // =============================================== // public methods // =============================================== /** * Loads the component with the specified name. * * @param name The name of the component * @return The component <tt>Class</tt> object * @throws ComponentNotFoundException If the * component * was not * found */ public Class loadComponent(String name) throws ComponentNotFoundException { Class cls = findLoadedComponent(name); if (cls != null) { return cls; } cls = findComponent(name); if (cls != null) { componentCache.put(name, cls); return cls; } throw new ComponentNotFoundException( "Definition for " + "component: " + name + " was not found"); } /** * Loads and instantiates the component with the * specified name. * * @param componentName * @return The component <tt>Class</tt> object * @throws ComponentNotFoundException */ public Object createComponent(String componentName) throws ComponentNotFoundException { Class cls = loadComponent(componentName); try { Object componentObj = cls.newInstance(); return componentObj; } catch (Exception e) { throw new ComponentNotFoundException( "Unable to" + " instantiate " + " component: " + componentName); } } } 特定のクラスバージョンのロードさて、コンテナと制限付きクラスローダを使って、バージョン付きのクラスを格納しているコンポーネントを特定のアーカイブからロードできるようになりました。 リスト2に、コンポーネントコンテナのテストクラスのコードを示します。このコードでは、コンポーネントコンテナのインスタンスを2つ作成し、それぞれを2つの異なるバージョンのHelloWorldコンポーネントに使う別々の設定ファイルの名前で初期化しています。その後、 リスト2 コンポーネントコンテナのテストクラス
package com.jeffhanson.components; import junit.framework.*; import java.net.URL; public class ComponentContainerTest extends TestCase { // ============================================== // static methods // ============================================== public static junit.framework.Test suite() { return new TestSuite( ComponentContainerTest.class); } // ============================================== // constructors // =============================================== public ComponentContainerTest(String name) { super(name); } // ============================================== // non-public methods // ============================================== private String doLoadComponentTest( ComponentContainer container, String componentName) throws ComponentNotFoundException { Object componentObj = container.createComponent(componentName); return componentObj.toString(); } // ============================================== // public methods // ============================================== public void setUp() throws Exception { super.setUp(); } public void tearDown() throws Exception { super.tearDown(); } public void testLoadComponent1() throws Exception { String componentName = "com.jeffhanson.components.HelloWorld"; ComponentContainer container = new ComponentContainer( "HelloWorldV1-CDI.xml"); String componentStr = doLoadComponentTest(container, componentName); assertNotNull(componentStr); assertTrue(componentStr.equals( "Hello world V1!")); container = new ComponentContainer( "HelloWorldV2-CDI.xml"); componentStr = doLoadComponentTest( container, componentName); assertNotNull(componentStr); assertTrue(componentStr.equals( "Hello world V2!")); } } それぞれのコンポーネントオブジェクトのインスタンスを呼び出すと、適切なバージョンのコンポーネントによって処理されます。 図3のシーケンス図は、フレームワークがコンポーネントをロードおよび作成する際の手順を示しています。 これで、クラスローディングを行うコンポーネントコンテナフレームワークの作成方法の説明は終わりです。このフレームワークによって、Javaコンポーネントの定義、バージョン管理、作成を行うための自己完結的なコンテキストを簡単に実現できるようになります。Javaのクラスローディング機能をこのような形で利用すると、クラスローディングを特定の場所に限定できるので、異なるバージョンのクラスを同時にロードできるようになります(これらのクラスはどちらも同じJVM上で作成、使用されます)。 著者紹介Jeff Hanson(Jeff Hanson)
ソフトウェア業界で18年以上の経験を持つ。これまでにWindows OpenDocポートの上級エンジニア、NovellのRoute 66フレームワークの主席設計者を務める。現在は、J2EEベースの再保険システム用のフレームワークとプラットフォームの提供を専門とするeReinsureの主任設計者。著書、執筆記事多数。
|