NoClassDefFoundErrorの原因と対応(2)
前回はTomcatにおけるクラスローダの全体像を書いた。今回はクラスの参照可否性について説明したい。クラスの参照可否性とはクラスローダがクラスを読み込むことができるかどうかの条件で、少々複雑な内容になる。
前回説明したクラスローダの階層と今回説明する参照可否条件まで知っておけば、クラスローダに起因する問題には大部分対応できるハズだ。今回はサンプルコードを元に説明したい。
関連記事: Javaクラスローダの恐怖!影響範囲は一体どこまで!?
サンプルServletの紹介
準備したコードはJakarta HTTP Clientを使用したServlet。これは http://localhost:8080/ にHTTP接続した結果をそのまま返すというもので、ソースは以下の通りだ。
public class HttpClientServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpClient client = new HttpClient(); GetMethod method = new GetMethod("http://localhost:8080/"); client.executeMethod(method); String contentType = method. getResponseHeader("Content-Type").getValue(); byte[] b = method.getResponseBody(); response.setContentType(contentType); response.getOutputStream().write(b); method.releaseConnection(); } }
HTTP Clientを使う場合は、HTTP Client本体のライブラリとは別に、HTTP Client自身が参照するライブラリが2つ必要だ。そこでWEB-INF/lib内に以下のライブラリを配置した。
- commons-httpclient-3.1.jar
- commons-logging.jar
- commons-codec-1.3.jar
このHttpClientServletにブラウザからアクセスすると、TomcatのTOPページが正常に表示された。特に問題もない。
ライブラリの場所を移動すると・・・
さて、ここからが本題。実験として、Tomcat共通クラスローダの管理下(<TOMCAT>/lib)にライブラリを移動したとき、動作がどのように変化するのかを見てみる。
HTTP Clientのみを<TOMCAT>/libへ移動
まず、ライブラリ commons-httpclient-3.1.jar を<TOMCAT>/lib ディレクトリに移動してみた。その後、HttpClientServletにアクセスすると次のようなエラーが。
commons loggingのライブラリは<WEBAPP>/WEB-INF/lib内に配置してあるにもかかわらず、クラスLogFactoryの定義が見つからない、というエラーが出てしまった。
commons loggingも<TOMCAT>/libへ移動
今度はcommons loggingのjarファイルを<TOMCAT>/lib ディレクトリに移動した。すると、commons codec内のクラス定義が見つからないというエラーに。
全てのライブラリを<TOMCAT>/libへ移動
最後はcommons codecも含めて全てのライブラリを移動することに。結果は正常だった。
3つのどのパターンでも、ライブラリはすべてクラスローダが読み込み対象とするディレクトリに配置している。にもかかわらず、クラス定義が見つからないというエラーになった理由とは何なのか?
子クラスローダのクラスは読み込めない!
NoClassDefFoundErrorが発生したのは、子クラスローダが読み込み対象としているクラスを親クラスローダから参照できないから。とはいっても、この条件は文章だけではなかなか伝わりづらい。
クラスの読み込みに成功したケース・失敗したケースとで、どのクラスローダがどのクラスを読み込んでいるのかを図で確認してみる。
最初に成功したケース
全てのライブラリをWEB-INF/lib内に配置したとき、クラスローダとクラスの関係は次のようになる(一部のクラスだけを記載)。
まずHttpClientServletクラスから考えていくと、次のようになる。
- HttpClientServletクラスはWebアプリケーションクラスローダが読み込む。
- 次にHttpClientServletクラスはWebアプリケーションクラスローダを利用してHttpClientクラスを読み込む(矢印①)。
- HttpClientクラスも同じクラスローダを利用してLogFactoryクラスを読み込む(矢印②)。
- その後、HttpClientServletクラスはWebアプリケーションクラスローダを利用してGetMethodクラスを読み込む(矢印③)。
- GetMethodクラスもWebアプリケーションクラスローダを利用してURLCodecクラスを読み込む(矢印④)。
HTTP ClientやHTTP Clientの関連ライブラリは、全てWebアプリケーションクラスローダが読み込んでいることになる。
HTTP ClientだけTOMCAT/libに移動してエラーになったケース
HTTP Clientだけを移動した場合は、次のように状況が変化する。
HttpClientServletクラスからの動きは以下の通り。
- HttpClientServletクラスはWebアプリケーションクラスローダが読み込む。
- HttpClientServletはWebアプリケーションクラスローダを利用し、HttpClientクラスを読み込む(矢印①)。
- HttpClientはTomcat共通クラスローダを利用し、LogFactoryを読み込もうとする(矢印②)。
しかしクラスローダがクラスを読み込む際のルールは、「親クラスローダにクラス読み込みを任せ、見つからなければ自分自身のクラスローダで読み込む」が原則だ。そしてクラスを読み込む際には、現在実行中のクラスを読み込んだクラスローダを利用して読み込む。Tomcat共通クラスローダから見ると、Webアプリケーションクラスローダは子クラスローダになるから、LogFactoryクラスはHttpClientクラスから参照できない。LogFactoryが見つからないという例外が発生したのは、HttpClientクラスから見えない場所にLogFactoryクラスが存在したからだ。
ここでのポイントは、あるクラスから参照可能なクラスは、そのクラスを読み込んだクラスローダ自身もしくはその子クラスローダが読み込み可能なクラスに限られる、という点にある。あるクラスが参照するクラスは、同一クラスローダ内か、その上位のクラスローダに配置しておく必要があるのだ。
まとめ
子クラスローダにクラスの読み込みを委譲しないという仕様は、これもJavaのセキュリティポリシーに関係しているに違いない。子クラスローダはClassLoaderクラスを実装するだけで作成できるから、もし親クラスローダが子クラスローダにクラス読み込みを委譲できてしまうと、これまで親クラスローダ内に存在していたクラスの動作を子クラスローダによって変更できてしまう恐れがある。
そして <TOMCAT>/lib 内にアプリケーションで使うライブラリを配置することは通常推奨されていないけど、実際には諸々の事情でライブラリを配置しなければならない場合がある。そんなときは、依存関係のあるライブラリも全て <TOMCAT>/lib 内に配置しなければクラスを読み込むことができずに NoClassDefFoundError となる。これはJavaのセキュリティポリシーによるものだ。外部ライブラリを使用する際には、依存関係のあるライブラリにも十分着目していかなければならないだろう。
Javaアプリケーションサーバーでライブラリを使用する際には、配置場所による優先順位と参照可否性を理解しておきたい。
(参考)