japan.internet.com
japan.internet.com メンバーID
Twitter
Facebook
RSS
ピックアップ
2006年8月8日 12:00

Javaのクラスローディングを制御する

著者Jeff Hansonオリジナル版を読む海外海外発

はじめに

 Javaのクラスローディングフレームワークは、強力かつ柔軟です。このクラスローディングフレームワークを使えば、アプリケーションがクラスライブラリにアクセスする際に、静的な「インクルード」ファイルにリンクする必要がなくなります。その代わりに、指定の場所(例えばCLASSPATH環境変数で定義したディレクトリやネットワークロケーションなど)から、ライブラリクラスを格納したアーカイブファイルとリソースがロードされます。このシステムにより、実行時のクラスとリソースへの参照が動的に解決されるため、更新と改訂バージョンのリリース作業が簡略化されます。それでも、各ライブラリは独自の依存関係をひととおり持っており、アプリケーションが正しいバージョンを確実にうまく参照できるかどうかは、開発者と導入担当者次第です。あいにく、既定のクラスローディングシステムと特定の依存関係はバグやシステムクラッシュ、あるいはもっと悪い事態を引き起こす可能性があります(というよりも実際に引き起こしています)。

 本稿では、このような問題を解決するためのクラスローディングコンテナフレームワークを紹介します。

Javaのクラスパス

 Javaでは、ランタイムがクラスや他のリソースを必要に応じて検索する際のパスを指定するのに環境プロパティ/変数、それにCLASSPATHを使用します。CLASSPATHを適切に設定するには、CLASSPATH環境変数を設定するか、Javaのコマンドラインオプションである-classpathを使います。

 一般に、Javaのランタイムは次の順序でクラスを探し、ロードします。

  1. ブートストラップクラスのリストに記載されているクラス
  2. これらは、例えば「rt.jar」内のクラスのように、Javaプラットフォームを統合するクラスです。
  3. 拡張クラスのリストに記載されているクラス
  4. これらのクラスではJava、プラットフォームを拡張するExtension Mechanism Frameworkというフレームワークを、.jarや.zipなど、/lib/extランタイム環境ディレクトリにあるアーカイブファイルと共に使用します。
  5. ユーザークラス
  6. これらのクラスでは、-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コンポーネントをアーカイブの中にパッケージ化してデプロイするという処理がうまくいっているのは、次の条件が成立しているからです。

  • インスタンス化するコンポーネントのバージョンを開発者が明確に指定できる。
  • コンポーネントの補助クラスの適切なバージョンを、コンポーネントと同じjarファイル内の情報に基づいて正しくロードできる。

 これにより、どのバージョンのコンポーネントを実際に作成して使用するかをコンポーネントの開発者と使用者が完全に制御できます。

 以降では、コンポーネントとコンポーネントの名前空間を、どのアーカイブに格納するかによって定義するという考え方について説明します。

補助リソースの共有

 標準クロスローダを使ってJavaの共有ライブラリを扱う場合の最大の問題は、すべてのクラスが単一の名前空間にロードされてしまうことです。そのせいで、同じライブラリの別々のバージョンを任意のタイミングで使い分けるのは、非常に困難です。コンポーネントや補助ライブラリのロード先となる独自の名前空間を、開発者が自分で定義できる必要があります。

 JavaのクラスのランタイムIDはクラスの完全修飾名とクラスローダのIDによって定義されるので、個々のクラスローダには既に名前空間があります。従って、そのクラスローダを利用して、コンポーネント(およびその依存コンポーネント)の名前空間を定義するコンポーネントコンテナを作成することができます。

 例えば、「com.jeffhanson.components.HelloWorld」という名前のクラスがあり、このクラスを2種類のバージョンで動かしたいとします。この場合の解決策は、それぞれのバージョンを別々のクラスローダで作成することです。この概念を図1に示します。

図1 複数のクロスローダの使用:Javaの命名規則の仕組みにより、別々のクロスローダを使うと別々の名前空間が定義される
図1 複数のクロスローダの使用:Javaの命名規則の仕組みにより、別々のクロスローダを使うと別々の名前空間が定義される

 後で例を紹介しますが、1つのクラスを2つの異なるクラスローダでインスタンス化するというテクニックでは、実際には1つの仮想名前空間が作成されます。ただし本稿の例では、同じバージョンのクラスのインスタンスを複数作成しただけです。

 同じクラスの複数のバージョンをロードしてインスタンス化する処理を容易にするために、以降では、クラスローダの名前空間メカニズムに基づいて同じクラスの別々のバージョンをロードできるようにしたコンポーネントコンテナフレームワークの例を紹介します。

クラスローダの名前空間を利用する

 コンポーネントコンテナフレームワークという概念は、jarまたはzipアーカイブで定義されたコンポーネントとそれらのコンポーネントが必要とする補助クラスをロードする役割を持ったコンテナエンティティとして実装できます。このフレームワークの目的は次のとおりです。

  1. インスタンス化するコンポーネントのバージョンを開発者が明確に指定できるようにすること
  2. 各コンポーネント用の適切な補助クラスを、コンポーネントと同じjarファイル内の情報に基づいて正しくロードすること
  3. 補助クラスとアーカイブをコンポーネント全体で共有すること

 コンポーネントとそれに関連する補助ファイルを定義するには、次のような設定ファイルが必要です。

<?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つ紹介します。上の例と、次の例の要素を比べてみてください。変更されている唯一の点は、component-archive要素の値だけです。それぞれのバージョンのコンポーネントを格納するアーカイブの名前は、この要素の値で定義します。

<?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>

 フレームワークがクラスをロードする際に、指定した場所からのみロードするよう保証するためには、URLClassLoaderを拡張して新しいクラスローダを作成する必要があります。まず、loadClassメソッドをオーバーライドして、そのメソッドへの呼び出しが既定のクラスローダの親に伝播されることを防止します(これは結果的に、標準のクラスパスからのロードを防止することにつながります)。これにより、クラスローダに指定したURLでのみクラス検索が行われるようになるので、後はコンポーネントのロード元となる特定のjarファイルの場所をクラスローダに与えればよいことになります。

 次のコードは、この制限付きクラスローダのメカニズムを示しています。

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に、コンポーネントコンテナのテストクラスのコードを示します。このコードでは、コンポーネントコンテナのインスタンスを2つ作成し、それぞれを2つの異なるバージョンのHelloWorldコンポーネントに使う別々の設定ファイルの名前で初期化しています。その後、ComponentContainerクラスのcreateComponentメソッドによって各バージョンのコンポーネントがロードされ、インスタンス化されます。

リスト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のシーケンス図は、フレームワークがコンポーネントをロードおよび作成する際の手順を示しています。

図3 コンポーネントのシーケンス。コンポーネントコンテナフレームワークがコンポーネントを作成する際のシーケンスを示している
図3 コンポーネントのシーケンス。コンポーネントコンテナフレームワークがコンポーネントを作成する際のシーケンスを示している

 RectrictedURLClassLoaderクラスへの呼び出しが、既定のクラスローダのインスタンスに到達せずに終了していることに注目してください。これにより、RestrictedURLClassLoaderインスタンスに与えられたURLでのみクラス検索が行われます。

 これで、クラスローディングを行うコンポーネントコンテナフレームワークの作成方法の説明は終わりです。このフレームワークによって、Javaコンポーネントの定義、バージョン管理、作成を行うための自己完結的なコンテキストを簡単に実現できるようになります。Javaのクラスローディング機能をこのような形で利用すると、クラスローディングを特定の場所に限定できるので、異なるバージョンのクラスを同時にロードできるようになります(これらのクラスはどちらも同じJVM上で作成、使用されます)。

著者紹介

Jeff Hanson(Jeff Hanson)
ソフトウェア業界で18年以上の経験を持つ。これまでにWindows OpenDocポートの上級エンジニア、NovellのRoute 66フレームワークの主席設計者を務める。現在は、J2EEベースの再保険システム用のフレームワークとプラットフォームの提供を専門とするeReinsureの主任設計者。著書、執筆記事多数。

関連テーマ
プリンター用
記事を転送
この記事をクリップ!
【特別連載企画】au 版「GALAXY」の実力は?--ISW11SC 速攻レビュー
【特別連載企画】au 版「GALAXY」の実力は?--ISW11SC 速攻レビュー KDDI(au)から、NTT ドコモの人気スマートフォン「GALAXY S II」を大幅に進展させた「GALAXY SII WiMAX ISW11SC」が発売される。サムスンがこれまでに蓄積してきたノウハウが詰まった本機の実力をレポートする。
⇒詳細記事はこちら
⇒連載記事一覧はこちら
注目のトピックス
最新コラム一覧
百式のネットビジネス研究
百式のネットビジネス研究
フリーランスな人が多い今だからこそ…「FREELANCE THANKS」
アウンのグローバルマーケティング動向
アウンのグローバルマーケティング動向
Web プロモーションにおいて大切なこと―年度末編―
週刊-サイト別アクセス状況データ
週刊-サイト別アクセス状況データ
12月の主婦層、ベルメゾンが首位を維持(VRI 調査)
多言語×Web×海外マーケティング情報
多言語×Web×海外マーケティング情報
海外発、注目 AR プロモーション
エンジニア転職ノウハウ開発室
エンジニア転職ノウハウ開発室
楽天が目指す変革──Globalization、Agile、Big Data
中国・台湾ネットビジネス情報最前線
中国・台湾ネットビジネス情報最前線
中国から Web を見てもらいたいならば
マーケティングに活用できる最新トレンド
マーケティングに活用できる最新トレンド
改めて、「導線」最適化に目を向ける
次世代マーケティングチェーンの視点
次世代マーケティングチェーンの視点
ソーシャル時代における BtoC 型 Eコマース成功のポイント
Copyright 2012 internet.com K.K. (Japan) All Rights Reserved.