2005年12月14日(水)
日本アイ・ビー・エム株式会社 ソフトウェア事業部
夷藤勇人
全5回シリーズ「クラスローダーとJ2EEパッケージング戦略を理解する」、第3回となる前回は、クラスローダーがもたらすドッペルゲンガー現象について学びました。
第4回となる今回は、ディフェンシブ・パッケージングについて解説します。ディフェンシブ・パッケージングとは、上位クラスローダー配下に置いてある「クラスやリソース」の影響を受けないように、自アプリケーションに防衛策を施しておくパッケージング戦略のことです。アプリケーションのディフェンスが弱いと、容易に「侵入」を許してしまい、予期せぬ動作をしてしまうことがあります。
今回は、ロギング・フレームワークとして有名なJakartaプロジェクトのcommons-logging を例として取り上げます。commons-loggingを使用したWebアプリケーションのパッケージング例です。
リスト1 commons-loggingを使用したWebアプリケーション
luv-war
- WEB-INF/classes
- com/example/MyHello
- ...
- WEB-INF/lib
- commons-logging.jar
commons-loggingの使用例です。
リスト2 commons-loggingの使用例:MyHelloクラス
...
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class MyHello {
static final Log log = LogFactory.getLog(MyHello.class);
void hello() {
log.info("Hello");
}
...
}
commons-loggingは他のロギング・ライブラリーへのラッパーとして動作します。commons-loggingと組み合わせるものとしては、
Apacheプロジェクトのロギング・ライブラリー・Log4j
J2SE1.4から導入された標準のjava.util.loggingパッケージ
などが一般的です。今回の例では明示的に何を使用するかを指定していません。この場合は、通常、java.util.loggingパッケージが自動的に使用されます。
アプリケーションでは、commons-loggingで提供されているクラス
org.apache.commons.logging.Log;
org.apache.commons.logging.LogFactory;
のみを使用しますが、実際にはjava.util.loggingパッケージを利用する実装が選択され、ロギングそのものはjava.util.loggingパッケージングが最終的に行います。
一見、何の問題もないようですが、WebSphere Application Server V6(またはV5)上でリスト1のWebアプリケーションを動作させると、エラーが発生してしまいます(リスト3)。
リスト3実行結果エラーメッセージ
...
Caused by: org.apache.commons.logging.LogConfigurationException:
The chosen LogFactory implementation does not extend LogFactory.
Please check your configuration.
(Caused by java.lang.ClassCastException: com.ibm.ws.commons.logging.TrLogFactory)
...
エラーメッセージからは
1.
LogFactoryの実装として「com.ibm.ws.commons.logging.TrLogLogFactory」が選択された
2.
選択されたTrLogFactoryはLogFactoryのサブクラスではない
このため「ClassCastException」がおきてしまったことが読み取れます。
「TrLogFactory」とは、WebSphereが独自に提供しているcommons-loggingのログファクトリーの実装クラスです。なぜ意図に反してこの実装が選択されたのでしょうか? 原因は、commons-logging.propertiesファイルにあります。このプロパティーファイルは、commons-loggingがロギング実装を選択する際に使用されます。
commons-logging.propertiesがどこにあるのか確かめてみましょう。:
getClass().getClassLoader.getResource("commons-logging.properties")
結果
jar:file:/.....<WAS_HOME>/lib/ws-commons-logging.jar!
/commons-logging.properties
commons-logging.propertiesファイルは、WebSphereのインストールディレクトリー([WAS_HOME])の下、libフォルダ内のJarファイルws-commons-logging.jarの中に含まれていることがわかります。
このプロパティーファイルの内容はこのようになっています:
org.apache.commons.logging.LogFactory=com.ibm.ws.commons.logging.TrLogFactory
commons-loggingのログファクトリーの実装として、TrLogFactoryが選択されたのは、このためです。WebSphereでは「TrLogFactory」をcommons-loggingのロギング実装として使用するように強制的に指定されているのです。
これは、上位クラスローダー配下においてある「クラスやリソース」のため、下位クラスローダー配下のアプリケーションが影響を受けてしまう典型的な例です。
先ほどエラーメッセージに出ていた
1.
LogFactoryの実装として「com.ibm.ws.commons.logging.TrLogLogFactory」が選択された
これは原因が理解できました。
それでは、もうひとつ
2.
選択されたTrLogFactoryはLogFactoryのサブクラスではない
これはなぜでしょうか?
前回、解説したドッペルゲンガーがここでも登場しているのです。クラスローダーの観点から、この現象を見てみましょう(図1)。
WebSphere EXTクラスローダー配下([WAS_HOME]/lib)には、commons-loggingのAPI(commons-logging-api.jar)と、そのWebSphere版の独自実装ws-commons-logging.jarが標準で付属しています。
その結果として、TrLogFactoryが継承するのは
(A)
(WebSphere EXTクラスローダーがロードした)org.apache.commons.logging.LogFactory
になります。
一方、クラスローダーのデリゲーションモードが「親が最後(PARENT LAST)」の場合は、Webアプリケーションから最初に見つかるLogFactoryは、自アプリケーション内(WEB-INF/lib)にいれておいた
(B)
(WARクラスローダーがロードした)org.apache.commons.logging.LogFactory
になります。
TrLogFactoryは(A)を継承しています。(B)にキャストすることはできません。これがClassCastExceptionの原因です。
「WebSphereでは独自にcommons-loggingの実装を提供しているため、アプリケーション内ではcommons-loggingを使用できない」という誤った認識が広がる結果になりました。
このように上位クラスローダー配下に「何か」があっただけで、アプリケーションが影響を受けて動かなくなるのでは困ります。アプリケーションの上位クラスローダーに何がおいてあるか・その環境はアプリケーション・サーバーによって大きく異なりますし、同じアプリケーション・サーバーであってもバージョンによって異なります。不完全な他のアプリケーションを動作させるために、アプリケーション・サーバー管理者が「好ましくない」ライブラリーを、全アプリケーションから見える位置においていることもありえます。
アプリケーション作成側としては、自アプリケーション内に防衛策をとる必要があります。
今回、例としてとりあげたcommons-loggingの場合は、対策は容易です。最も簡単な方法は、アプリケーション内にcommons-logging.propertiesを用意しておくことです。
例として、commons-loggingとLog4jと組み合わせる場合の、Webアプリケーションパッケージング例をリスト4に示します。
リスト4 commons-loggingとLog4jの組み合わせ
luv.war
- WEB-INF/classes
- commons-logging.properties
- log4j.xml
- WEB-INF/lib
- commons-logging.jar
- log4j.jar
commons-logging.propertiesには、以下のように明示的にLog4jによる実装を指定しておきます
org.apache.commons.logging.LogFactory=
org.apache.commons.logging.impl.Log4jFactory
(※実際は一行)
Log4jではなく、java.util.loggingパッケージを使用する場合は、以下のようになるでしょう。
org.apache.commons.logging.LogFactory=
org.apache.commons.logging.impl.Jdk14Logger
(※実際は一行)
WAR単体ではなく、EAR全体でひとつのcommons-loggingを使用する場合はこのようになります。
リスト5 EAR全体でcommons-logging + Log4jを使用する
luv-app.ear
- commons-logging.jar
- log4j.jar
- utility.jar
- commons-logging.properties
- log4j.xml
- ejb1.jar
- luv1.war
- luv2.war
クラスローダーの観点から見た場合は以下のようになります。
commons-logging.propertiesファイルやLog4Jの設定ファイルは、全J2EEモジュールから共通に見える箇所、すなわちアプリケーションクラスローダー配下に配置します。たとえば、ユーティリティーJARの中などがその候補になります。
こうしておけば、ひとつの設定ファイルで全モジュールのロギングの設定を行えるようになります。
今日のオープンソースを最大限に活用するアプリケーション開発においては、「WEB-INF/libにおいてあるjarファイルがいつの間にか30個を超えていた」というのも決して珍しい状況ではありません。オープンソースライブラリーは、アプリケーションだけが使用するものではなく、アプリケーション・サーバー側でも活用しているケースが非常に多いです。
これらのライブラリー間の依存関係を正確に把握することは、パッケージング戦略において最重要項目のひとつです。しかし、実行時にクラスが見つかるかどうか・利用できるかどうかによって、動作を変更するライブラリーが多いことが正確な把握を一層困難にします。
残念ながら、これらの「JAR地獄(JAR HELL)」を解決する銀の弾丸は存在しません。クラスローダーに関する知識を武器に、地道にディフェンスあるのみです。
次回、最終回となる、「クラスローダーとJ2EEパッケージング戦略を理解する - 第5回 To Divide, Or Not?」では、最初にコンテキスト・クラスローダーについて解説します。その後、これまでに筆者が遭遇した現実のプロジェクトで採用されているJ2EEパッケージング戦略を実例としてとりあげて解説します。これまではひとつのEAR内での話が主でしたが、最終回では現実での運用を考えた上でのEAR分割戦略、WAR分割戦略、モジュール分割戦略とその評価をしていきます。
著者である夷藤氏は、現在IBMにおいて、WebSphere Application Serverの技術支援を担当しており、多くのJ2EEプロジェクトにおいてシステムデザインやアプリケーション開発の助言を行っています。また、業界標準パフォーマンス評価団体、SPEC におけるJ2EEアプリケーション・サーバー評価システム、SpecJAppServer2002 の開発を行っていました。
専門はJava/J2EEですが、彼の興味はサーバーサイドのみならずクライアントサイド・テクノロジー、Python, Eclipseなどへと多岐に渡っており、雑誌「Eclipseパーフェクトマニュアル」でのテストファースト・プログラミングに関する記事執筆や、Eclipse - RCP やJava/J2EE に関する講演活動などでもおなじみです。