新しい IBM Developer JP サイトへようこそ!サイトのデザインが一新され、旧 developerWorks のコンテンツも統合されました。 詳細はこちら

クラスローダーとJ2EEパッケージング戦略を理解する: 第1回 クラスローダーを理解する – クラスはどこからやってきた?

はじめに

エンタープライズ・アプリケーションを作成して、いざアプリケーション・サーバーで実行。そして出るのはお約束の「java.lang.ClassNotFoundException...」。 Java/J2EEの開発者なら誰もが見たことのある例外でしょう。「おかしいな。このクラスはこのjarに入っているはずなのに。どうして見つからないんだろ?」クラスローディングに関する問題は多くのJ2EE開発者にとって今でも悩みの種です。 すべてのJ2EE開発者が正しいJ2EEパッケージ戦略を理解し、誤った戦略をとることのないことを願って、新シリーズ「クラスローダーとJ2EEパッケージング戦略を理解する」は執筆が開始されました。全5回に分けてお送りします。

J2EEパッケージング戦略とは

J2EEパッケージング戦略とは、あまり聞きなれない言葉でしょう。このシリーズでは、J2EEパッケージング戦略という言葉を、J2EEアプリケーションのパッケージング単位であるEARをどのように構成するか、すなわち、

  • ejb-jarやWARなど各モジュールの粒度をどのようにするか
  • 各モジュールの依存関係をどうするか
  • ライブラリーの配置
  • EARを分割するべきかどうか
  • クラスローダーの設定

これらの問題にどう立ち向かうか?という意思決定・戦略のことをいいます。

あえて戦略という大げさな言葉を用いたのは、これらが想像以上に重要な問題だということを認識してほしいからです。冒頭に述べた「ClassNotFoundException」等の単純なエラーを防ぐのはもちろんですが、J2EEパッケージング戦略は、それ以上に各コンポーネント、各アプリケーション・レイヤー間の依存関係、システム全体のデザインにもかかわってくる問題です。さらに、現実の多くのJ2EE開発プロジェクトにとっては、開発体制そのもの、果ては本番稼働後の運用体制・方針をも左右することになる最重要問題です。

シリーズの第1回目となる今回は、正しいJ2EEパッケージング戦略を理解するうえで、なにより重要な、そして全ての基本となる「クラスローダー」について学びます。

どうしてクラスローダーについて学ばなければならないのか

クラスローダーとは、その名が示すとおりクラスをロードするものです。日ごろ、我々Java開発者はなにも意識することなくクラスを使用していますが、クラスは使用される前に必ずクラスローダーによってJVM(Java Virtual Machine)のメモリ空間上にロードされます。長年、Javaに携わっている人でも、クラスローダーの存在を気にも留めていないという人は多いでしょう。それでは、我々J2EE開発者は、普段は意識する必要のないクラスローダーについて、知っておく必要があるのでしょうか?答えはYesです。正しいJ2EEパッケージング戦略をとるためには、クラスローダーに関する知識が不可欠です。

実際、WebSphere開発者が集まる掲示板、WebSphere Forumにも、クラスローダーに関する知識が必要とされる質問が投稿されています。

  • WSAD5.1でcommons-logging使用について教えてください。
  • WAR Classloader ポリシーの設定について。
  • ライブラリーの配置
  • WSAD5.1で、commons-loggingを使うとEJBがLoggerを参照出来なくなる。
  • xmlsecロード時のExceptionInInitializeについて

クラスローダーを自分の味方にして、正しいJ2EEパッケージング戦略をとることによって問題を解決できることに、多くのJ2EE開発者は気づいていません。予期せぬClassNotFoundExceptionClassCastExceptionに対抗するためには、きっちりと理論武装をしておき正しく対処する必要があります。

クラスローダー、はじめの一歩

クラスローダーというと特別なものに思えるかも知れませんが、恐れる必要はありません。クラスローダーを身近に感じるためにもまずはクラスローダーを使用したちょっとしたテストを行ってみましょう。リスト1をご覧ください。

リスト1. クラスローダー、はじめの一歩

public class SimpleTest {

    public static void main(String[] args) {

        SimpleTest simpleTest = new SimpleTest();
        ClassLoader cl = simpleTest.getClass().getClassLoader();
        System.out.println("SimpleTest.class is loaded by " + cl);
        System.out.println("java/lang/String.class is found at "
                + cl.getResource("java/lang/String.class"));
    }

}

リスト1を順にみていきましょう。

リスト1-1. クラスローダーを取得する

        SimpleTest simpleTest = new SimpleTest();
        ClassLoader cl = simpleTest.getClass().getClassLoader();
        System.out.println("SimpleTest.class is loaded by " + cl);

このようにクラスローダーは、プログラムの中からでも取得が可能です。ここでは、java.lang.ClassのgetClassLoader()メソッドを使用することによって、該当クラス、すなわちSimpleTestクラスを、ロードしたクラスローダーを取得します。 System.out.println の出力結果から、クラスローダーは実際には、

SimpleTest.class is loaded by sun.misc.Launcher$AppClassLoader@3c6d53c4

であることがわかります。 次に、取得したクラスローダーに何か「リソース」を探させてみます。

リスト1-2. クラスローダーを使用してリソースを探す

         System.out.println("java/lang/String.class is found at "
                + cl.getResource("java/lang/String.class"));

ClassLoaderのgetResource(String)メソッドは、プロパティーファイル(*.properties)等を読み込むためによく使用されますが、今回の例のようにクラスファイルそのもの(*.class)を探すことも可能です。リソースを探すメソッドですので、リソース名としては、"java.lang.String"ではなく、"java/lang/String.class"とファイル名そのものを引数として与えていることにご注意ください。結果はリソースの場所をあらわすURLです。

java/lang/String.class is found at jar:file:/C:/opt/rad/runtimes
/base_v6/java/jre/lib/core.jar!/java/lang/String.class

java.lang.Stringのクラスファイルは、Javaの<JRE>/lib/core.jarの中に入っていることがこの出力結果から読み取れます。単純なやり方ですが、このようにクラスローダーを使用すると、実際にクラスがファイルシステム上のどこからロードされるかを調べるのに役立ちます。

そして混沌の世界へ – クラスローダーヒエラルキー

クラスローダーが1つしかないのなら、話は簡単です。しかし、今日では、1つのJVM内にクラスローダーが複数存在するのが普通です。これはJ2EEアプリケーション・サーバーの世界も例外ではありません。例として、WebSphere Application Server内では、どのようなクラスローダー構成になっているのか、図1をご覧ください。

alt

図1のように、複数のクラスローダーたちは親子関係をなしています。一番の親に当たる部分がブート・ストラップ・クラスローダーです。このクラスローダーは <JRE>/lib/*.jar 等のJavaのコアAPIに関するクラス(java.lang.* 等)をロードします。続いて、その子供にあたるのが、エクステンション・ディレクトリー内、すわなち <JRE>/lib/ext/*.jar 内のクラスをロードするエクステンション・クラスローダーです。さらに環境変数 CLASSPATH で指定されているクラスをロードするシステム・クラスローダーが、後に続きます。ここまでは、通常のJVM内なら必ずといっていいほど存在するクラスローダーです。

ここから先の子供たちが、WebSphere Application Server特有のクラスローダーになります。まず、WebSphere Application Server本体のクラスをロードする、WebSphere EXT クラスローダーです。このクラスローダーは、<WAS_HOME>/lib/*.jar 等のWebSphere Application Serverそのものが使用するクラスをロードします。

その下にあるのが、アプリケーション・クラスローダーです。これは、アプリケーション・サーバーにインストールされたエンタープライズ・アプリケーション、すなわちEARごとに1つずつ専用に割り振られるクラスローダーです。この例では、アプリケーションが2つ、bank.eartrading.earがインストールされていますので、それぞれ専用のアプリケーション・クラスローダーが割り当てられます。アプリケーション・クラスローダーの役目は、各EAR内に含まれるejb-jarやユーティリティーjar内のクラスをロードすることです。

アプリケーション・クラスローダーの下には、アプリケーションに含まれるWARモジュール、すなわちWebアプリケーションごとにWARクラスローダーが1つずつ割り当てられます。各WARクラスローダーは、WARモジュール内の WEB-INF/classes 以下や WEB-INF/lib/*.jar 内のクラスのロードを担当します。

このように、それぞれのクラスローダーには、どこへクラスを探しにいくかという「ローカルクラスパス」というものが存在すると考えるとわかりやすくなるでしょう。それぞれ守備範囲が決まっているのです。

クラスローダー – デリゲーション・モデルを理解する

クラスローダーを理解するうえで最も重要な概念は、「デリゲーションモデル」です。クラスローダーは、必要に応じて親クラスローダーにクラスのロードをお願い、つまりお任せ、委譲(デリゲート)するのです。たとえば、システム・クラスローダーがあるクラスをロードする必要があったとします。この時、システム・クラスローダーは、自分のローカルクラスパス内、すなわち環境変数CLASSPATH内を探す前に、最初に親であるエクステンション・クラスローダーに、クラスのロードを依頼します。同じようにエクステンション・クラスローダーも、まず親であるブート・ストラップ・クラスローダーにクラスのロードをデリゲートします。親クラスローダーで見つかった場合は、そこで終了します。親で見つからなかったときになって、はじめて子供は自分のローカルクラスパス内を探しにいき、クラスをロードしようとします。デリゲーションモデルで重要なことは、決して子供にお願いすることはないということです。上にたどることはあっても、下を探すことはありません。

デリゲーションモデルを理解するために、簡単なテスト用EARを用意して、アプリケーション・サーバー上でテストを行ってみましょう。以下のような構成のEARを用意します。この EAR(luv-app.ear)には、モジュールとして ejb-jar(luv-ejb.jar)WAR(luv.war)が1つずつ入っています。

リスト2. EAR構成

 luv-app.ear
    - luv-ejb.jar
       - com.example.ejb.InEJB
    - luv.war
        - WEB-INF/
          - classes/
            - com.example.web.ClassLoaderTestServlet
            - com.example.web.InWeb

各モジュール内の各クラスをロードするのは、どのクラスローダーでしょうか?クラスローダーの守備範囲の観点で見た場合、図2のようになります。

alt

クラスInEJBとInWebは実際には空のクラスです。

package com.example.ejb;
public class InEJB {
}
package com.example.web;
public class InWeb {
}

テスト用サーブレット、ClassLoaderTestServletは以下のような内容です。

リスト3. ClassLoaderTestServlet

package com.example.web;
...(略)...
import com.example.ejb.InEJB;

public class ClassLoaderTestServlet extends HttpServlet {

    protected void service(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        InWeb inWeb = new InWeb();
        ClassLoader warClassLoader = inWeb.getClass().getClassLoader();
        testClassLoader(warClassLoader);

        InEJB inEJB = new InEJB();
        ClassLoader appClassLoader = inEJB.getClass().getClassLoader();
        testClassLoader(appClassLoader);
    }


    public static void testClassLoader(ClassLoader cl) {

        System.out.println("ClassLoader: " + cl);

        String[] classFiles = {
                "java/lang/String.class",
                "com/example/ejb/InEJB.class",
                "com/example/web/InWeb.class"
                };

        for (int i = 0; i < classFiles.length; i++) {
            String classFile = classFiles[i];
            System.out.println("Resource name : " + classFile);
            System.out.println("     Location : " + cl.getResource(classFile));
        }
    }

}

このサーブレットは、テストをキックするためだけに用います。テスト自体は、先ほどと同様にクラスローダーを使用してリソースを探す内容になっています。先ほどと異なり、今回は、2つのクラスローダー、すなわちWARクラスローダー、

         InWeb inWeb = new InWeb();
        ClassLoader warClassLoader = inWeb.getClass().getClassLoader();

と、アプリケーション・クラスローダー、

        InEJB inEJB = new InEJB();
        ClassLoader appClassLoader = inEJB.getClass().getClassLoader();

を取得して、それぞれリソースを見つけることができるかどうかテストします。検索対象のリソースは以下のとおりです。

  • java/lang/String.class(JREのコアランタイムjar内に存在)
  • com/example/web/InEJB.class(ejb-jar内に存在)
  • com/example/web/InWeb.class(WAR内に存在)

各クラスローダーは、リソースを発見することができるでしょうか?サーブレットを実行した結果、ログには以下のように出力されます(見やすいように、整形しています)。

ClassLoader:
com.ibm.ws.classloader.CompoundClassLoader@1f7010c3
   Local ClassPath: ...(略)...\installedApps\lemonNode02Cell\luv-app.ear\luv.war
   \WEB-INF\classes;
                    ...(略)...\installedApps\lemonNode02Cell\luv-app.ear\luv.war;
   Delegation Mode: PARENT_FIRST

Resource name : java/lang/String.class
     Location : jar:file:/C:/opt/rad/runtimes/base_v6/java/jre/lib/core.jar!/java
     /lang/String.class

Resource name : com/example/ejb/InEJB.class
     Location : wsjar:file:/C:/opt/...(略).../installedApps/lemonNode02Cell
     /luv-app.ear/luv-ejb.jar!/com/example/ejb/InEJB.class

Resource name : com/example/web/InWeb.class
     Location : file:/C:/opt...(略).../installedApps/lemonNode02Cell/luv-app.ear
     /luv.war/WEB-INF/classes/com/example/web/InWeb.class


ClassLoader:
com.ibm.ws.classloader.CompoundClassLoader@1f6250c3
   Local ClassPath: ...(略)...\installedApps\lemonNode02Cell\luv-app.ear
   \luv-ejb.jar;
   Delegation Mode: PARENT_FIRST

Resource name : java/lang/String.class
     Location : jar:file:/C:/opt/rad/runtimes/base_v6/java/jre/lib/core.jar!/java
     /lang/String.class

Resource name : com/example/ejb/InEJB.class
     Location : wsjar:file:/C:/opt/...(略).../installedApps/lemonNode02Cell
     /luv-app.ear/luv-ejb.jar!/com/example/ejb/InEJB.class

Resource name : com/example/web/InWeb.class
     Location : null

結果をまとめますと、表1のようになります。

表1. 各クラスローダーのリソース検索結果(○は見つかった、×は見つからなかった)

  アプリケーション クラスローダー WAR クラスローダー
java/lang/String.class
com/example/ejb/InEJB.class
com/example/web/InWeb.class ×

出力結果から、WARクラスローダーは、すべてのクラスファイルを発見できたことがわかります。WARクラスローダーのローカルクラスパス内には、InEJBクラスや、Stringクラスはありません。それにもかかわらず見つけることができるのは、デリゲーションモデルのおかげです。自分、自分の親にあるクラスは発見できます。

対照的に、アプリケーション・クラスローダーは、子供であるWARクラスローダー配下に存在するクラスInWebを発見できませんでした。デリゲーションモデルはあくまで親クラスローダーにデリゲーションするのであって、決して子供にはデリゲートしないため、このような結果になるのです。

原点に立ち戻って考える。誰がクラスを探しにいく?

「クラスローダーがクラスを探すことができない」ということはわかりましたが、このことは一体何を意味するのでしょうか?Javaの原点に立ち戻って考えて見ましょう。クラスとクラスローダーの関係について、以下のルールが存在します。

ルール1 : クラスローディングの際、使用されるクラスローダーの決定方法

あるクラスAの内部で別のクラスBを使用するときは、「クラスAをロードしたクラスローダー(CL-Aとします)」が起点となりクラスBをロードしようとします。この際、デリゲーションモデルにより、CL-Aの親クラスローダーにはデリゲーションが発生しますが、CL-Aの子供にはデリゲーションは発生しません。

わかりにくいのでこのルールを具体的な例にあてはめて実感してみましょう。もう一度先ほどと同じEARを使用してテストを行ってみます。変更点として、先ほどは空だったクラスInEJBを修正します。クラスInEJBがクラスInWebを内部で使用するように変更を加えましょう。

package com.example.web;
public class InWeb {
}
package com.example.ejb;
...
public class InEJB {
    private InWeb inWeb = new InWeb();
}

さらにテストサーブレットを以下のように単純化して、実行してみます。

リスト4

package com.example.web;
...(略)...
import com.example.ejb.InEJB;

public class ClassLoaderTestServlet extends HttpServlet {

    protected void service(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        System.out.println("inWeb : " + new InWeb());  // (1)
        System.out.println("inEJB : " + new InEJB());  // (2)
    }
}

一見、何も問題がないようですので、このサーブレットをキックすると、

inWeb: com.example.web.InWeb@3e25a5
inEJB: com.example.ejb.InEJB@xxxxxx

とログに出力されると思うかもしれません。しかし、実際の出力結果は以下のようになります。

inWeb: com.example.web.InWeb@3e25a5
サーブレット ClassLoaderTestServlet で service() メソッドを呼び出せませんでした。
スローされた例外: java.lang.NoClassDefFoundError: com/example/web/InWeb
        at com.example.ejb.InEJB.<init>(InEJB.java:10)
        at com.example.web.ClassLoaderTestServlet.service(ClassLoader
        TestServlet.java:20)
        ...(略)

なんとNoClassDefFountErrorが発生してしまいました。どうして、InWebクラスが見つからないのか、不思議に思われるかもしれません。エラーがでる直前には、

inWeb: com.example.web.InWeb@3e25a5

と表示されています。つまりリスト4の(1)の部分は、問題なく実行できており、クラスInWebはサーブレットからは確かに見つかっています。それにもかかわらず、次の行、リスト4の(2)の部分を実行しようとすると、InWebクラスが見つからないとエラーになってしまうのです。先ほどは見つかっていたクラスが、とたんに見つからなくなってしまうのです。

この現象を説明するのが、先ほど紹介したルール1です。ルールにあてはめて考えてみましょう。まず、うまくいった部分(1)を振り返ってみましょう。ルールにあてはめてみると、

クラスClassLoaderTestServletがクラスInWebを使用するときは、クラスClassLoaderTestServletをロードしたクラスローダー、すなわちWARクラスローダーが、クラスInWebを探しにいきます。

WARクラスローダーは、クラスInWebを見つけることができますので、問題ありません。次に(2)の部分を考えて見ましょう。ルールにあてはめてみると、

クラスClassLoaderTestServletがクラスInEJBを使用するときは、クラスClassLoaderTestServletをロードしたクラスローダー、すなわちWARクラスローダーが、クラスInEJBを探しにいきます。

WARクラスローダーは、クラスInEJBを見つけることができます。といっても実際にはデリゲーションモデルにより、クラスInEJBをロードするのはアプリケーション・クラスローダーになります。ここまでは問題ありません。問題はクラスInEJBの中身です。

public class InEJB {
    private InWeb inWeb = new InWeb();
}

これをルールにあてはめますと、

クラスInEJBがクラスInWebを使用するときは、クラスInEJBをロードしたクラスローダー、すなわちアプリケーション・クラスローダーが、クラスInWeb探しにいきます。

おわかりでしょうか?既に見たように、アプリケーション・クラスローダーは、子供クラスローダー配下に存在するクラスInWebを見つけることができません。そのため、クラスInEJBを使用しようとしたとたん、該当エラー、NoClassDefFoundErrorが発生してしまうのです。この例のように、あるクラスが、下位クラスローダー配下しか存在しないクラスに依存しているという構成が、そもそもの失敗の原因だったわけです。

まとめ

今回の第1回では、クラスローダーの基本について学びました。クラスローダーがどのような働きをし、クラスがどのようにロードされるかを知っておくことは、正しいJ2EEパッケージング戦略をとる上で非常に重要です。

次回の第2回も、しつこいようですがクラスローダーそのものが話題です。「クラスローダーとJ2EEパッケージング戦略を理解する – 第2回 クラスローダーを理解する – シングルトンがシングルトンでなくなる日」では、クラスローダーのアイソレーション設定、デリゲーションモードの設定など、より重要な概念を紹介します。クラスローダーの設定が原因で発生する一見摩訶不思議な挙動を起こすプログラムを例として紹介し、第3回以降で詳しく解説予定のJ2EEパッケージング戦略を理解する上での基礎を固めていきます。