NoClassDefFoundErrorの原因と対応(1)

NoClassDefFoundErrorの原因と対応(1)

前回の記事ではJavaクラスローダに関する現象を書いた。普段はあまり意識することの少ないクラスローダだけど、前回のような現象が現実に起きていることを考えると、クラスローダに対する多少の知識はあったほうが絶対に役立つ。

今回はクラスローダの様々な機能・特徴のうち、Webアプリケーションプログラマでも是非知っておきたい情報を2回に分けてまとめてみた。キチンと理解できればTomcatでNoClassDefFoundErrorが起きてもすぐに解決できるようになるはずだ。

Javaの関連記事:

クラスを読み込むのがクラスローダ

クラスローダは名前の通り、クラスファイル(*.class)やライブラリ(*.jar)からクラスを読み込むために使用される。と言っても、実際はクラスを表すファイルだけではなく、プロパティファイル(*.properties)やXMLファイルなど何でも読み込み可能だ。

クラスローダは1つだけではない

Java起動時には、役割に応じた3種類のクラスローダが存在する。

  • ブートストラップ・クラスローダ:Javaのコアクラス(java.*パッケージ、javax.*パッケージ)をロードする
  • 拡張クラス・クラスローダ:JREの拡張ディレクトリ(<JRE_HOME>/lib/ext)内のクラスをロードする
  • システム・クラスローダ:CLASSPATH環境変数や-classpath引数で指定されたディレクトリやjarファイル内からクラスをロードする

Java実行時にはこれらのクラスローダを使ってクラスを読み込んでいる。で、どのクラスローダを使ってもクラスが見つからないときにNoClassDefErrorやClassNotFoundExceptionが発生する。

クラスローダはユーザーによる定義も可能

先ほど「Java起動時にクラスローダは3種類存在する」と言ったけど、クラスローダはアプリケーションごとに独自に定義することができる。

例えばTomcat 6の場合、Tomcat共通クラスローダとWebアプリケーションクラスローダの2種類のクラスローダがある。Tomcat共通クラスローダは<TOMCAT_HOME>/lib内のライブラリを読み込むためのもの。WebアプリケーションクラスローダはWEB-INF内のclassファイルやjarファイルを読み込むクラスローダで、Webアプリケーションごとに1つずつ作成される。

classloader1

そして図のようにクラスローダはツリー構造をなしている。

「親のクラスローダ優先」が原則

クラスローダは委譲モデルを採用している。あるクラスローダでクラスが見つからなかったら、他のクラスローダがクラスの読み込みを行い、そこでも見つからなかったら次のクラスローダへ・・・という感じ。

そしてクラスローダの優先順は「子より親」。

子のクラスローダを優先させたほうが便利に思えるけど、セキュリティ上の理由で親クラスローダが優先されている。というのも、子のクラスローダを優先させた場合、java.*パッケージやjavax.*パッケージにある重要なクラスまで置き換えが可能となってしまい、Javaプログラムが正常に動作しなくなる可能性があるからだ。

Tomcatはちょっとだけ特殊

先ほど説明したルールによると、Tomcatでは最優先されるのがブートストラップクラスローダで、最後に回されるのがWebアプリケーションクラスローダになるように思える。

が、実際は次のように親の「Tomcat共通クラスローダ」よりも子の「Webアプリケーションクラスローダ」が優先される(赤丸が優先順位)。

classloader2

例えば同名のクラスが <TOMCAT>/lib 内と <WEBAPP>/WEB-INF/lib 内の両方に存在するときは、Webアプリケーションクラスローダが優先されるから <WEBAPP>/WEB-INF/lib 内のクラスが使われることになる。

なぜこうなっているかと言うと、Servlet仕様書でWebアプリケーションサーバーの共通ライブラリよりもWebアプリケーション内のクラス・ライブラリを優先することが推奨されているから。そこでTomcatのクラスローダはServlet仕様に合致するようにクラスローダが実装されているのだ。

<WEBAPP>/WEB-INFが優先されるというルールはぜひ覚えておきたい。

実際のクラスローダを確認してみよう

ClassLoaderクラスを使えば、実際のクラスローダの階層を知ることができる。次のようなServletを書いてみた。

public class ClassLoaderDumpServlet extends HttpServlet {
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
      response.setContentType("text/plain");
      PrintWriter writer = response.getWriter();
      ClassLoader cl = this.getClass().getClassLoader();
      while (cl != null) {
        writer.println(cl.getClass().getName());
        cl = cl.getParent();
      }
    }
  }
}

Tomcat6での実行結果は次の通り(子クラスローダから親の順)。ブートストラップクラスローダはAPIから参照できないようだが、それ以外のクラスローダは表示されていた。

classloader3

まとめ

今回はTomcatを使用した場合にどのようなクラスローダが存在しているのか、を中心に説明した。これでどのディレクトリにクラスパスが通っているのか、どういう順番に読み込まれるのか、全体像がつかめたのではないかと思う。

次回はクラスローダにさらに踏み込んでみます。

(参考)

Page Top