2009년 9월 2일 수요일

the difference from class#getResource and ClassLoader#getResouce

public static Properties getProperties(String filePath) throws IOException{
  //ClassLoader로 InputStream 생성  
  //InputStream is = ClassLoader.getSystemResourceAsStream(filePath);
  InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
  Properties props = new Properties();
  props.load(is);
  return props;
}


getClass().getResourceAsStream(filePath) 와 같음.

단 getClass().getResourceAsStream(filePath) 는 static context에서 사용할 수 없음.


Class::getResource(name) 와
ClassLoader::getResource(name) 은 차이가 있다.

ClassLoader.getResource(name)의 'name'은 '/'로 시작할 수 없고 항상 상대경로로 인식한다.
반면 Class.getResource(name)의 'name'은 '/'로 시작해야만 절대경로가 된다.


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() を利用すると、物理パスを取得できるので、書き出し用ディレクトリの取得もできたりします。ただし、この方法でディレクトリを見つける場合は、クラスパスに複数ディレクトリが設定されると危険であるということを念頭におく必要があります。

댓글 없음:

댓글 쓰기