Class#getResource と ClassLoader#getResource の違いと怪しさ

Java の実装の話です。

設定ファイルやら相対パスを使ったリソース参照のために、よく getResource() や getResourceAsStream() を用います。
これらに与えるリソース名について Class#getResource() のドキュメントでは以下のような説明がされています。

委譲の前に、このアルゴリズムを使って指定されたリソース名から絶対リソース名が構築されます。

  • name が「/」(「\u002f」) で始まる場合、リソースの絶対名は「/」に続く name の部分である
  • そうでない場合、絶対名は以下の形式になる
    modified_package_name/name
    ここで、modified_package_name は、「. 」(「\u002e 」) を「/」に置き換えたこのオブジェクトのパッケージ名になる

これは実装を忠実に表しています。しかし ClassLoader#getResource() のほうではどのような記法でどこからロードを行うかは記載されていません。ClassLoader は抽象クラスであり、どのように検索を行うかは記述できないのです。ただ、先頭の / の扱いに関しては規定しておいて欲しかった…。

Class#getResource() でも最終的には ClassLoader#getResource() が呼び出されます。しかし、このメソッド自体が、クラスローダの実装によって振る舞いが異なるという問題があります。

リソース名の先頭に "/" を書くか書かないかと、Class#getResource() を呼ぶか ClassLoader#getResource() を呼ぶかで、Sun の JDK(1.4/1.5) に付属の ClassLoader 実装では以下のような挙動になります。

Class#
getResource()
ClassLoader#
getResource()
"/name"ルートから name を検索見つからない
"name"クラスのパッケージディレクトリ相対で name を検索ルートから name を検索

さて、皆さんこのあたりはご存知だったでしょうか?

ClassLoader#getResource() で "/" から始まるパスを指定した場合は有無を言わさずリソースが見つかりません。これはバグとしか思えませんが、ClassLoader#getResource() の検索仕様は明確に決められていないので、仕様ということもできてしまいます。
(ちなみに実装がどうなっているかというと、ディレクトリベースの場合は new URL(baseDirUrl, "/name") という処理が行われ、この結果が壊れます。jar の場合は JarEntry の取得に失敗します)

また、Class#getResource のような振る舞いを期待して ClassLoader#getResource() を呼び出してしまうと、クラスの位置ではなくクラスパスのルートからの相対になるため、謎のバグに見舞われたり、混乱を招いたりします。

これだけならまだいいのですが、アプリケーションサーバなどで独自のクラスローダを用いているものでは、先頭が "/" であっても取得できてしまったりすることがあります(orion server 等で確認)。未確認ですが、先頭の "/" がないと取得できないようなものがもし存在すると、JDK との間のポータビリティが完全に失われます。

最も安全なのは、常に Class#getResource() を用いるということです。これならクラスローダ実装の差異を吸収してくれます。
この場合、相対指定のときにどのパッケージディレクトリになるかというのが、対象の Class クラスによって変わるので、深く考えずに getClass().getResource("相対パス") と書く癖があると落とし穴にはまるかもしれませんのでそこだけはご注意を。

この整理を試みたのは、hibernate が内部的に ClassLoader#getResource() を使ってマッピングファイル読み込みを行っていることが原因で、先頭に "/" を書いていて、且つスタンドアローンでの dbunit での起動時のみマッピングファイルが読み込めないといった現象になったためです。hibernate が Class#getResource() を使っていてくれるか、あるいは JDK のバグとして仕様の明確化&Sun 実装の修正がなされれば良いわけですね。

また、おまけになりますが、ClassLoader#getResource() の場合、検索のルートディレクトリより上位にあるリソースは取得できませんが、URL#getFile() を利用すると、物理パスを取得できるので、書き出し用ディレクトリの取得もできたりします。ただし、この方法でディレクトリを見つける場合は、クラスパスに複数ディレクトリが設定されると危険であるということを念頭におく必要があります。

トラックバック(1)

このブログ記事を参照しているブログ一覧: Class#getResource と ClassLoader#getResource の違いと怪しさ

このブログ記事に対するトラックバックURL: http://www.sk-jp.com/mt/mt-tb.cgi/340

というかdiconファイルのパス指定が、aaa/bbbの様にスラッシュから始まらないのはいかがなものかと思い。ソースを見てみると予想通り、Thread#getContextClassLoader().getResource()を使っている。普通にコンテナを起動すると、sun.misc.Launcher.AppClassLoader...... 続きを読む

コメント(2)

非常に貴重な情報有り難うございます。
ちょうど自分もここの箇所を実験してたところで、「ClassLoader#getResource()が動かないよ~~」と悲鳴を上げてたところです。試しにgetClass().getResource()でやったら一発でした・・・。(あと、nameの表記法が分からなかったのもあったみたいです。)

でも、www.koders.comとかでgetResourceで検索した上位ソースコードって、みんなClassLoader#getResource()使ってるみたいです。ううう、そっちの方が「行儀が良い」んですかねえ?でも確実に動いてくれるのはClass#getResourceなんですよね?

いずれにせよ、貴重なレポートに大感謝です!有り難うございました!

非常に貴重な情報有り難うございます。
ちょうど自分もここの箇所を実験してたところで、「ClassLoader#getResource()が動かないよ~~」と悲鳴を上げてたところです。試しにgetClass().getResource()でやったら一発でした・・・。(あと、nameの表記法が分からなかったのもあったみたいです。)

でも、www.koders.comとかでgetResourceで検索した上位ソースコードって、みんなClassLoader#getResource()使ってるみたいです。ううう、そっちの方が「行儀が良い」んですかねえ?でも確実に動いてくれるのはClass#getResourceなんですよね?

いずれにせよ、貴重なレポートに大感謝です!有り難うございました!

コメントする

このブログ記事について

このページは、Shinが2005年4月 5日 22:49に書いたブログ記事です。

ひとつ前のブログ記事は「バックアップ前のデータベースを消しちゃった」です。

次のブログ記事は「ブログがTV/新聞を超えて市民権を獲得するなんて無理」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。