目次


クラスローダーとJ2EEパッケージング戦略を理解する

第2回「クラスローダーを理解する - シングルトンがシングルトンでなくなる日」

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: クラスローダーとJ2EEパッケージング戦略を理解する

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:クラスローダーとJ2EEパッケージング戦略を理解する

このシリーズの続きに乞うご期待。

はじめに

全5回シリーズ「クラスローダーとJ2EEパッケージング戦略を理解する」の第1回となる前回は、Java・クラスローダーの基本となるデリゲーション・モデルについて学びました。第2回となる今回も予告したとおり、クラスローダーそのものが話題です。クラスローダーの設定によって、引き起こされる摩訶不思議な現象に迫っていきます。
今回登場する新しい概念は、「デリゲーション・モード」です。

デリゲーション・モードとは

最初にリスト1をご覧ください。これは、WARクラスローダーそのものを単純に標準出力に表示してみた結果です。該当クラスローダーに関する情報が出力されるように、クラスローダーのtoString()メソッドがオーバーライドされていることがわかります。

リスト1:WARクラスローダーの設定 - メソッドtoString()の結果

 
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

出力結果から、該当クラスローダーの「ローカル・クラスパス(Local ClassPath)」を知ることができます。このWARクラスローダーがどこへクラスを探しにいくかがわかります。今回、注目したいのは、出力結果の次の行です。

 
  Delegation Mode: PARENT_FIRST

これがクラスローダーの「デリゲーション・モード(Delegation Mode)」です。この例では、デリゲーション・モードが「PARENT FIRST(親が最初)」に設定されています。

「PARENT FIRST(親が最初)」か?「PARENT LAST(親が最後)」か?

デリゲーション・モードとは何でしょうか?前回を振り返ってみましょう。前回、クラスローダーのデリゲーション・モデルにおいては、「各クラスローダーは自分のローカル・クラスパスを探す前に、最初に親クラスローダーにクラスのロードを依頼する」と説明しました。実はこの挙動は、クラスローダーのデリゲーション・モードがデフォルト値である「PARENT FIRST(親が最初)」の場合です。

前回説明したクラスローダー・ツリーを振り返ってみましょう(図1)。

図1:クラスローダー・ツリー
図1
図1

図1で登場するクラスローダーのうち、アプリケーション・クラスローダーとWARクラスローダーについては、デリゲーション・モードをカスタマイズすることが可能です。デリゲーション・モードの設定は2種類です。「PARENT FIRST(親が最初)」と「PARENT LAST(親が最後)」です。

  • 「PARENT FIRST(親が最初)」(デフォルト値)の場合 自クラスローダーのローカル・クラスパスを探す前に、最初に親クラスローダーにデリゲートします。親で見つからなかったときにはじめて、自分のローカル・クラスパスを探しに行きます。
  • 「PARENT LAST(親が最後)」の場合 自クラスローダーのローカル・クラスパスを最初に探します。見つからなかったときにはじめて、親クラスローダーにデリゲートします。

デリゲーション・モードの設定によってクラスを探す順番が変わってきます。WARクラスローダーがあるクラスをロードする場合、表1に示す順番で各クラスローダーがクラスを探すことになります。

表1:デリゲーション・モード設定による、クラス検索順序の変化
表1:デリゲーション・モード設定による、クラス検索順序の変化
表1:デリゲーション・モード設定による、クラス検索順序の変化

例えば、デフォルト値である「(A)APP-FIRST、WAR-FIRST」、すなわち、アプリケーション・クラスローダー、WARクラスローダーどちらも「PARENT FIRST」の場合は、探す順番はクラスローダー・ツリーの上から下へ単純に降りてくることになります。

注釈:なお、WebSphere Application Serverの管理コンソール上では、デリゲーション・モードのことは、クラス・ローダー・モード(Class loader mode)と表記されています(図2、3)。この連載シリーズでは、直接意味が伝わりやすいように、クラスローダー・モードというより、デリゲーション・モードという言葉を使用します。
図2:アプリケーション・クラスローダーの設定
図2
図2
図3:WARクラスローダーの設定
図3
図3

いったい「PARENT LAST(親が最後)」がいつ必要になるっていうのだい?

なぜ「PARENT LAST」にする必要があるのでしょうか?以下に示す、とある日の悲劇を例にとって考えてみましょう。

とある日の悲劇:無謀なチャレンジャー

  
Aさん:「Jakartaプロジェクトのライブラリー ABC-1.2.jarを使用しているのですけど、どうもおかしな挙動なのです。」
Bさん:「ちゃんとWEB-INF/libにおいてる?」
Aさん:「おいています。クラスは確かに見つかっているんです。」
・・・(30分後)
Aさん:「調査してみました。WebSphereのインストールしたディレクトリー(WAS_HOME)下のlibディレクトリー内に同じライブラリーABCの古いバージョンABC-1.1.jarが置いてありました。自分のアプリケーション内に置いておいたWEB-INF/lib/ABC-1.2.jarではなくそちらが先に使用されているようです。」
Bさん:「まずいじゃない。ということは、ABC 1.2はWebSphereでは使用できないってこと?」
Aさん:「名案があります。WAS_HOME/lib/ABC-1.1.jarをABC-1.2.jarで置き換えちゃいましょう。」
Bさん:「いいね。やってみて。」
Aさん:「はい。」
・・・(5分後)
Aさん:「問題が発生しました。
・・・WebSphere自体が立ち上がらなくなってしまいました。」

「PARENT LAST」の存在意義は、まさにこの冗談のような無茶がもたらす悲劇を2度と繰り返さないためです。Aさんがやるべきことは、WARクラスローダーのデリゲーション・モードを「PARENT LAST」に変更することでした。WebSphereそのものに影響を全く与えることなく、自分のWebアプリケーション内に含まれるライブラリーが先に優先されるようになります。これこそが、クラスローダー・ツリーがもたらすメリットです。クラスローダーが複数あることの意味がおわかりでしょうか?

「PARENT LAST」にする必要性だって?いつでもさ。

WebSphere Application Serverが内部でどんなライブラリーを使用していようとも、それに影響されることなく自分の好きなライブラリーの最新バージョンを使用したいという欲求は開発者なら誰もが思うところです。「PARENT LAST」にするのは自然なことです。

実際、サーブレット仕様でもこのことについて触れている箇所があります。以下、サーブレット 2.4 Specification SRV.9.7.2からの引用です。

It is recommended also that the application class loader be implemented so that classes and resources packaged within the WAR are loaded in preference to classes and resources residing in container-wide library JARs.
アプリケーションのクラスをロードするクラスローダーは、アプリケーション・サーバー、Webコンテナ全体で使用するライブラリー内よりも、WAR内部のクラスやリソースを優先してロードすることが推奨される。

あくまで"recommended(推奨)"ですから、必須事項ではありません。ですから、WebSphere Application Serverにおいて、アプリケーション・クラスローダー、WARクラスローダーのデフォルト値が「PARENT FIRST」であることは、仕様に違反しているわけではないです。決して褒められたものではありませんが・・・。

この連載での推奨は、当然、表1の(D)、すなわちアプリケーション・クラスローダー、WARクラスローダーともに「PARENT LAST」に設定することです。

注釈:Java基本クラスローダーが担当するコアクラス(java.lang.*など)に関しては、たとえ「PARENT LAST」にしても、「上書き」できません。これはJavaのセキュリティー・モデルで禁止されています。これを許すと、攻撃者に内部攻撃の機会を与えてしまうことになります。例えば、java.lang.String等を勝手に書き換えられたら、java.lang.Stringの不変性に依存しているクラスなどは、その前提が破壊されていまうことになり、重大なセキュリティー・リスクを背負うことになります。

ちなみに、他のアプリケーション・サーバー、たとえばWebコンテナーであるTomcatはどうなっているか見てみましょう。「The Apache Jakarta Tomcat 5.5 Servlet/JSP Container Class Loader HOW-TO」からTomcatのクラスローダー・ツリーについて説明した図を以下に引用します(図4)。

The Apache Jakarta Tomcat 5.5 Servlet/JSP Container Class Loader HOW-TO (US)

図4:Tomcatにおけるクラスローダー・ツリー
図4:Tomcatにおけるクラスローダー・ツリー
図4:Tomcatにおけるクラスローダー・ツリー

Tomcatの場合は、EARが存在せず、Webアプリケーション(WAR)だけですので、若干の違いはあります。それでも、図4において、Tomcatでは「common」「share」配下よりも各「webapp」内のクラス・リソースが優先されると、ドキュメントには明記されています。Webアプリケーションのクラスローダーに関しては、やはり「PARENT LAST」の設定になっているわけです。

シングルトンがシングルトンでなくなる日

このように必要とされる「PARENT LAST」設定ですが、これは通常のJavaのクラスローダーのデリゲーション・モードとは異なる設定です。そのため普段はなかなかお目にかかることのできない不思議な現象を作り出すことができます。リスト2をご覧ください。

リスト2:典型的なシングルトンパターン - クラス com.example.Singleton

 
package com.example;
public class Singleton {
    
    private static Singleton instance = new Singleton();
    private int counter;
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
    
    public void increment(int i) {
        counter += i;
    }
    
    public int getCounter() {
        return counter;
    }

}

一見したところ何の変哲もないおなじみのシングルトン・パターンです。クラスcom.example.Singletonのインスタンスはひとつしか存在しないはずです。はたしてそうでしょうか?ここで、以下のルールを紹介しましょう。

クラスの同一性に関するルール

全く同じパッケージ名・同じ名前のクラスでも、違うクラスローダーからロードされたものは、同一クラスとは扱われません。別クラス扱いとなります。

このルールを使用すればシングルトンが唯一のものだという固定観念を破ることは簡単に行えます。サンプルとして以下のようなEARを用意してみましょう。

リスト3:EAR構成

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

シングルトン・クラスを、EJBモジュール、Webモジュール、両方に入れておきます。さらに、アプリケーション・クラスローダー、WARクラスローダー、共にデリゲーション・モードを「PARENT LAST」に設定します。クラスローダーの観点で見た場合は、図5のようになります。

図5:クラス配置図
図5:クラス配置図
図5:クラス配置図

この状態でシングルトンのテストをしてみましょう。テストコードはリスト4のようになります。

リスト4:シングルトンのcounter値チェックテスト

 
package com.example.web;

import com.example.Singleton;
import com.example.ejb.InEJB;
...
public class ClassLoaderTestServlet extends HttpServlet {

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

    private void testSingleton() {
        Singleton singleton = Singleton.getInstance();

        System.out.println("1. InWeb - counter: "
                         + singleton.getCounter());
        System.out.println("counter: +1");
        singleton.increment(1);
        System.out.println("2. InWeb - counter: " 
                         + singleton.getCounter());        
        InEJB.testSingleton();
        System.out.println("5. InWeb - counter: " 
                         + singleton.getCounter());
        
    }
}

package com.example.ejb;

import com.example.Singleton
...

public class InEJB {

    public static void testSingleton() {
        Singleton singleton = Singleton.getInstance();

        System.out.println("3. InEJB - counter: " 
                         + singleton.getCounter());
        System.out.println("counter: +10");
        singleton.increment(10);
        System.out.println("4. InEJB - counter: " 
                         + singleton.getCounter());
    }
    
    public static Singleton getSingleton() {
        return Singleton.getInstance();
    }
}

テストコードを見てみましょう。Webモジュール内のサーブレットClassLoaderTestServletと、EJBモジュール内のクラスInEJBの中では、どちらも同じシングルトン・クラスcom.example.Singletonを使用しています。シングルトン・インスタンスのcounter値を増加して値がどのように変化したか、そのつど表示しています。
このサーブレッドを実行してみましょう。シングルトンですので、結果はリスト5のようになると予想されます。

リスト5:シングルトンテスト - 予想実行結果

 
1. InWeb - counter: 0
counter: +1
2. InWeb - counter: 1
3. InEJB - counter: 1
counter: +10
4. InEJB - counter: 11
5. InWeb - counter: 11

しかし、実際の出力結果はリスト6のようになります。

リスト6:シングルトンテスト - 実際の実行結果

 
1. InWeb - counter: 0
counter: +1
2. InWeb - counter: 1
3. InEJB - counter: 0
counter: +10
4. InEJB - counter: 10
5. InWeb - counter: 1

実行結果からなにが読み取れるでしょうか?そうです。シングルトンはひとつではありません。Webモジュール内のサーブレット、EJBモジュール内のクラスInEJB、それぞれ違うシングルトンのインスタンスを見ているのです。図6をご覧ください。WARアプリケーション・クラスローダーのデリゲーション・モードを「PARENT LAST」に設定したため、サーブレットがシングルトン・クラスを探しにいくとき、最初に見つかるのは、WARクラスローダー内のシングルトン・クラスです。一方、クラスInEJBからは同じEJBモジュール内のシングルトン・クラスが最初に見つかります。
アプリケーション・クラスローダー、WARクラスローダー、それぞれのクラスローダーが、まったく同じ名前のクラスcom.example.Singletonをそれぞれ別々にロードすることになります。当然、シングルトン・クラスのstatic変数instanceも、それぞれのクラスに存在します。そのため、今回のように、シングルトンのインスタンスが2つ存在する結果となったのです。

図6:デリゲーション・モード - 共に「PARENT LAST」の場合
図6:デリゲーション・モード - 共に「PARENT LAST」の場合
図6:デリゲーション・モード - 共に「PARENT LAST」の場合

「PARENT FIRST」なら、このようなことは起きませんでした(図7)。たとえ、Webアプリケーション内にシングルトン・クラスを入れておいても、サーブレットからはアプリケーション・クラスローダー配下のシングルトン・クラスが先に見つかります。アプリケーション・クラスローダーがロードしたシングルトン・クラスが、両方で使用されることになります。そのため、シングルトンはまさに「JVM全体でシングルトン」の役割を果たすことになります。

図7:デリゲーション・モード - 共に「PARENT FIRST」の場合
図7:デリゲーション・モード - 共に「PARENT FIRST」の場合
図7:デリゲーション・モード - 共に「PARENT FIRST」の場合

ひとつのJVM内で、クラスを識別するキーとなるのは「パッケージ名+クラス名」ではないのです。「パッケージ名+クラス名+ロードしたクラスローダー」が識別のためのキーとなるのです。

まとめ

「クラスローダーとJ2EEパッケージング戦略を理解する」、第2回となる今回はクラスローダーのデリゲーション・モード、「PARENT LAST」の必要性、クラスの同一性に関する話題を扱いました。

次回の「ドッペルゲンガーの恐怖」では、いよいよJ2EEの世界で、EARをどのようにパッケージングすればよいか、具体的なパッケージング戦略について解説していきます。
ライブラリーはどこにおけばよいか、モジュール分割の方針、さらに世にも恐ろしいドッペルゲンガー現象について説明します。「デリゲーション・モードをPARENT LASTにして、WEB-INF/libにライブラリーは何でも気にせず全部放り込んでおけば大丈夫」という考え方がいかに危険であるか、解説していきたいと思います。

最後に余談になりますが、通常は不可能であるクラスローダー境界を超えた動的メソッドコールを、Dynamic Proxyを使用して実現するという「トリッキー」なテクニックがあります。第1回、第2回と解説したクラスローダーに関する基礎知識は、このような不可能を可能にする応用例にも生かすことにもできます。これらのテクニックは、数少ないJavaの醍醐味のうちのひとつです。
アプリケーション・サーバー以外の世界でも、Java開発者にとってクラスローダーに関する知識は、Javaを続けていく限り将来役にたつときが必ずやってくることでしょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=WebSphere
ArticleID=324987
ArticleTitle=クラスローダーとJ2EEパッケージング戦略を理解する: 第2回「クラスローダーを理解する - シングルトンがシングルトンでなくなる日」
publish-date=04202005