レベル: 上級 夷藤 勇人, ソフトウェア事業,
IBM
2006年 11月 22日 J2EE開発プロジェクトにおいて必要不可欠なJ2EEパッケージング戦略とはなにか?すべてのJ2EE開発者が正しいJ2EEパッケージング戦略をとれるように、詳細にその理論・方法を解説していきます。
はじめに
シリーズ「クラスローダーとJ2EEパッケージング戦略を理解する」、第5回はJava EEにおける「コンテキスト」について解説します。普段は、あまり意識することのないコンテキストですが、JavaのみならずJava EEの世界でも重要になる概念のひとつです。
リソースの読み込み
今回、例としてとりあげるのは、プロパティファイル(例:リスト1)の読み込みです。リスト2は、プロパティファイルをJavaで読み込む単純な例です。プロパティファイルの場所が、「/home/foo/bar/mudule.properties」であると想定しています。
リスト1: プロパティファイル module.properties
key1 = value1
key2 = value2
...
|
リスト2: プロパティファイルの読み込み
FileInputStream fis = null;
try {
fis = new FileInputStream("/home/foo/bar/mudule.properties");
Properties props = new Properties();
props.load(fis);
...
} catch (IOException e) {
...
} finally {
...
}
|
このように、プロパティファイルの場所を、アプリケーションから絶対パスで指定するのは避けるべきでしょう。それでは、プロパティファイルはどこに置き、アプリケーションからはどのようにその場所を指定すればよいでしょうか?シリーズ第1回の復習です。
プロパティファイルに代表されるいわゆる「リソース」は、クラスパス上においておけば、クラスローダーを使用して、絶対パスを意識することなく、相対パスで読み込むことが可能になります。
クラスローダーを用いた場合は、リスト3のようになります。メソッドgetProperties(String)は、
- パラメータとしてクラスパスからの相対位置を受け取り、
- クラスローダーを使用してプロパティファイルを読み込み、
- 読み込み済みのプロパティを戻り値として返す
メソッドです。ここでは、このユーティリティ・メソッドを含むクラスをPropertiesUtilと名づけておきます。
リスト3: クラスローダーを使用してプロパティファイルを読み込む
public class PropertiesUtil {
public Properties getProperties(String name) {
ClassLoader cl = getClass().getClassLoader();
InputStream is = cl.getResourceAsStream(name);
try {
Properties props = new Properties();
props.load(is);
return props;
} catch (IOException e) {
...
} finally {
...
}
}
|
このクラスPropertiesUtilの使用するクライアントは、リスト4のようになります。
リスト4: PropertiesUtilクラスの使用例
public class MyPojo {
PropertiesUtil propsUtil = ...; // get instance
Properties props;
void init() {
props = propsUtil.getProperties("module.properties");
...
}
|
Webアプリケーション・パッケージング
Webアプリケーションの場合も考え方は同様です。Webアプリケーションにおいてクラスパスに相当するのは「WEB-INF/classes」になります。パッケージング例は、リスト5のようになるでしょう。
リスト5: Webアプリケーションパッケージング例
- foo.war
- WEB-INF/classes/
- com/example/MyServlet
- com/example/MyPojo
- com/example/PropertiesUtil
- ...
- module.properties
|
Keep POJO
Webアプリケーションの場合は、プロパティファイルを使用しなくても、設定を「外出し」にする手段は多く用意されています。
デプロイメント・デスクリプタ(web.xml)内に設定値を書いておき、プログラム中から取得するには、以下のAPIが使用できます。
- javax.servlet.ServletConfig#getInitParameter(...)
- javax.servlet.ServletContext#getInitParameter(...)
また、WAR内に含まれるリソースを直接取得するAPIも用意されています。
- javax.servlet.ServletContext#getResourceAsStream(...)
ただしこれらはWebコンテナに依存した「サーブレットAPI」です。
他に代替手段があるにもかかわらずたかだか設定情報を読み込むくらいで、サーブレットAPIを使用するのは望ましいことではありません。
ビジネスロジックを担当するPOJOであればなおさらです。
サーブレットAPIに依存した瞬間、純粋な意味でのPOJOではなくなってしまいます。
エラー・ケース
次に、図1のように、EAR内にWebアプリケーションを2つパッケージングすることになったとしましょう。
図1 WARが2つのケース
各Webモジュールは、プロパティファイルとして同じ名前「module.properties」を使用しています。この場合も問題はありません。
それぞれのWARクラスローダーは、自分のWebアプリケーション内のリソースを読み込みますので、それぞれのプロパティファイルが混同することはありません。
それでは、図2のケースを考えてみましょう。EAR内の全てのモジュールから共通に使用するユーティリティクラスは、ひとつのJARとしてまとめることになりました。今回使用している、プロパティの読み込みを行うユーティリティクラスPropertiesUtilも、ユーティリティJAR(utility.jar)の中にいれて、EAR内にひとつだけ置くことにしました。
図2 PropertiesUtilクラスをユーティリティJAR化したEAR
パッケージングは、リスト6のようになります。
リスト6: ユーティリティJar化
foo.ear
- lib/
- utility.jar
- com/example/PropertiesUtil
- ...
- foo1.war
- WEB-INF/classes/
- com/example/MyServlet1
- com/example/MyPojo1
- module.properties
- foo2.war
- WEB-INF/classes/
- com/example/MyServlet1
- com/example/MyPojo2
- module.properties
|
この場合は、残念ながらプロパティファイルの読み込みに失敗します。PropertiesUtilクラスの該当部分をもう一度見てみましょう。
public class PropertiesUtil {
public Properties getProperties(String name) {
ClassLoader cl = getClass().getClassLoader();
InputStream is = cl.getResourceAsStream(name);
...
|
今回(図2)のケースでは、PropertiesUtilクラスは、アプリケーションクラスローダー配下にあります。そのため、この箇所で取得・使用されるクラスローダーは、クラス「Class<PropertiesUtil>」をロードしたクラスローダー、すわわちアプリケーションクラスローダーです。親クラスローダーであるアプリケーションクラスローダーは、子クラスローダー配下に存在するプロパティファイルを見つけることはできません。
今回のようなケース、すなわち
- 共通ユーティリティクラス、いわゆる「ライブラリ」は親クラスローダー配下に存在
- ライブラリを利用するクライアント、すなわちアプリケーションのクラスは子クラスローダー配下に存在
というのは、Java EEの世界に限った話ではなく、Javaでは非常によくある形態です。このような場合に、ライブラリ側から、クライアント側であるアプリケーション内のリソースを取得するにはどうすればよいでしょうか?
方法1: クラス(クラス・ローダー)を明示的に渡す
「ライブラリ側が、クライアント側のクラスローダーを、なんらかの方法で取得できればよい」のですから、まず最初に思いつく方法として、「リソースの読み込みに使用するクラスローダーを、ライブラリ側がクライアントから明示的に受け取る」ことが考えられます。この場合は、クライアント側はリスト7のようになります。クラスローダーを直接受け渡してもよいですが、ここではクライアント側の自クラスをライブラリ側に渡すことにします。
リスト7: クライアント側 - クラスを明示的に渡す
public class MyPojo {
PropertiesUtil propUtils = ...; // get instance
void init() {
Properties props = propUtils.getProperties("module.properties", getClass());
...
}
|
ライブラリ側では、渡されたクラスからクラスローダーを取得、リソースの読み込みに使用します(リスト8)。
リスト8: ライブラリ側 - 渡されたクラスからクラスローダーを取得して使用
public class PropertiesUtil {
public void Properties getProperties(String name, Class<?> clazz) {
ClassLoader cl = clazz.getClassLoader();
InputStream is = cl.getResourceAsStream(name);
...
}
|
方法2: コンテキスト・クラスローダーを用いる
次に示す方法は、先に示した方法1のように明示的にクラスやクラスローダーの受け渡しを行わなくても済む方法です。
鍵となるのは、「スレッド・コンテキスト」です。リスト9をご覧ください。
リスト9: コンテキスト・クラスローダーを用いる
public class PropertiesUtil {
public void Properties getProperties(String name) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
InputStream is = cl.getResourceAsStream(name);
...
}
|
もともとのPropertiesUtilクラス(リスト3)との違いは、以下の部分です。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
現在のスレッド(Thread.currentThread())にセットされている「コンテキスト・クラスローダー」を取得しています。方法1のように明示的にクラスやクラスローダーを受け取ることなく、「現在の呼び出し側」のクラスローダーを取得することが可能です。
それでは、コンテキスト・クラスローダー(正確には現在のスレッドのコンテキスト・クラスローダー - Current Thread Context ClassLoader [CTCC])は、どのように決定されるのでしょうか?一般には、スレッドが新しく作成されたときに、スレッドに対してコンテキスト・クラスローダーがひとつセットされます。では、Java EEの世界ではどのようにコンテキスト・クラスローダーが決定されるのでしょうか?
実例で理解するため、図3に示すEARを取り上げます。このEARを用いた場合、PropertyUtilsまでの呼び出しシーケンスと、その結果、クラスPropertyUtils内で取得できるコンテキスト・クラスローダーの対応を表1に示します。
図3 サンプルEAR
表1: 呼び出しシーケンスと取得できるコンテキスト・クラスローダーの対応
| ケースNo. | 呼び出しシーケンス | 取得できるコンテキスト・クラスローダー |
|---|
| 1 | (Httpリクエスト) -> MyServlet1 -> MyPojo1 -> PropertiesUtil | WARクラスローダー1 | | 2 | (Httpリクエスト) -> MyServlet2 -> MyPojo2 -> PropertiesUtil | WARクラスローダー2 | | 3 | EJBクライアント - (EJBコール) -> MySessionBean -> PropertiesUtil | アプリケーション・クラスローダー | | 4 | (Httpリクエスト) -> MyServlet1 -> MyPojo1 - (EJBコール) -> MySessionBean -> PropertiesUtil | アプリケーション・クラスローダー | | 5 | (Httpリクエスト) -> MyServlet1 -> MyPojo1 -> MyPojoInEJBJar -> PropertiesUtil | WARクラスローダー1 | | 6 | EJBクライアント - (EJBコール) -> MySessionBean -> MyPojoInEJBJar -> PropertiesUtil | アプリケーション・クラスローダー |
スレッド・コンテキストが変化するのは、サーブレットやEJB等のJava EEのコンポーネントを「正式に」呼び出した時、すなわち、HTTPリクエストを受けてサーブレットの処理が始まったときや、EJBコールを行ったときになります。ケース5のように、EJB JAR内の含まれるクラス「MyPojoInEJBJar」を単に経由するだけでは、スレッド・コンテキストが変わることはありません。この場合は、コンテキスト・クラスローダーはWARクラスローダーのまま変化しないことにご注意ください。
このように、コンテキスト・クラスローダーは、元々の呼び出し側のクラスローダーを取得する便利な方法です。Java EEアプリケーションで、「スレッド依存するAPIを使用しても大丈夫?」と疑問に持たれるかもしれませんが、実は、J2EE・Java EE仕様には、コンテキスト・クラスローダーの使用について明記されています。正しいコンテキスト・クラスローダーを返すことは、Java EEの仕様上、アプリケーション・サーバーの義務となっています。そのため、コンテキスト・クラスローダーの使用は、通常のJavaアプリケーションのみならず、Java EEでも通用する方法です。
アプリケーション・サーバーとスレッド・コンテキスト
このようにスレッド・コンテキストにもとずいて動作を変えるというのは、Java EEの世界では決して珍しいことではありません。
おなじみのJNDIのルックアップなどもスレッド・コンテキストによって動作を変える代表例です。リスト10に示すような、ENC(Environment Naming Context)に対するJNDIルックアップ、いわゆる"java:comp/env/.."に対するルックアップ(リスト10)も、スレッド・コンテキストに依存するAPIです。
リスト10 JNDI ENCに対するルックアップ:
initialContext.lookup("java:comp/env/Datasource")
|
サーブレット経由でルックアップしたか、それともEJB内でルックアップしたかによって、ルックアップの結果は異なります。
スレッド・コンテキストに依存するAPIが正しく動作するには、アプリケーション・サーバーがスレッド・プールを管理し、各スレッドがどのコンテキストに対応しているかを正しく把握できている必要があります。逆にいえば、アプリケーションが独自に作成したスレッドからは、ENCに対してJNDIルックアップしても成功する保障はありません。アプリケーションが独自に生成したスレッドには、「Java EEコンテキスト」が伝わっていないのです。Java EE仕様上、「アプリケーションが独自にスレッドを作成した場合、その動作が保障されていない」のは、この制限が大きな理由のひとつです。
もちろん、これらの制限を理解した上で、独自にスレッドを作成しているアプリケーションは多々あります。ですが、その使用はあくまで「自己責任」になります。
まとめ
今回は、スレッド・コンテキストそしてコンテキスト・クラスローダーについて解説しました。普段は意識することは少ないですが、Struts, Spring, Hibernate等のライブラリが、アプリケーション内の設定値・リソースを正しく取得できているのは、このコンテキスト・クラスローダーに負うところが多いです。
スレッド・コンテキストを上手に利用することで、「スレッドごとのグローバル変数」を擬似的に実現することができます。乱用は危険ですが、適切にスレッド・コンテキストを利用することは、APIの簡略化につながります。
シリーズ「クラスローダーとJ2EEパッケージング戦略を理解する」第6回となる次回からは、タイトルを「クラスローダーとJava EEパッケージング戦略を理解する」と変更し、これまでより実践向きの解説に移りたいと思います。まずは各アプリケーション・サーバーの最新動向のキャッチアップをします。OSGiベースとなり生まれ変わったWebSphere V6.1のクラスローダー・モデル、柔軟な設定とモダンなクラスローディングを可能にしているApache Geronimo 1.1、さらにJBossのユニファイド・クラスローダー等について取り上げます。そしてJ2EE1.4からJava EE 5における、パッケージングの観点からの変更点を解説します。
参考文献
より詳しく情報を得るためには、以下のリソースを参考にしてください。
著者について  | |  | 著者である夷藤氏は、現在IBMにおいて、WebSphere Application Serverの技術支援を担当しており、多くのJ2EEプロジェクトにおいてシステムデザインやアプリケーション開発の助言を行っています。また、業界標準パフォーマンス評価団体、
SPECにおけるJ2EEアプリケーション・サーバー評価システム、
SpecJAppServer2002の開発を行っていました。
SPEC (US)
SpecJAppServer2002 (US)
専門はJava/J2EEですが、彼の興味はサーバーサイドのみならずクライアントサイド・テクノロジー、Python、Eclipseなどへと多岐に渡っており、雑誌「Eclipseパーフェクトマニュアル」でのテストファースト・プログラミングに関する記事執筆や、Eclipse
- RCPやJava/J2EEに関する講演活動などでもおなじみです。
Eclipse - RCP
Java/J2EE |
記事の評価
|