Groovy の ClassLoader
Java で作ったツールを Groovy 上で動かそうとした時に ClassLoader 回りではまってしまったのでメモ
Java のクラスローダ
Java のクラスローダについては以下の通り。
まず Java のクラスローダのオブジェクトを取得してみる
public class ShowClassLoader { public static void main(String[] args) { ClassLoader loader = ShowClassLoader.class.getClassLoader(); while (loader != null) { System.out.println(loader); loader = loader.getParent(); } } }
継承関係ではなく、特定のクラスローダは親になるクラスローダを parent として保持しているため、getParent メソッドで親をたどれます。 ClassLoader の JavaDoc によると null を返す場合、親がブートストラップクラスローダーを表すとのこと。
sun.misc.Launcher$AppClassLoader@4e25154f sun.misc.Launcher$ExtClassLoader@33909752
※OracleJDK を使用しているため他の VM では名称や階層が異なるかもしれません。
AppClassLoader、 ExtClassLoader は URLClassLoader を継承しているようなので、getURLs メソッドで読み込んでいる クラス パスを表示できます。
import java.net.URL; import java.net.URLClassLoader; public class ShowClassPaths { public static void main(String[] args) { ClassLoader loader = ShowClassPaths.class.getClassLoader(); while (loader != null) { System.out.println(loader); if (loader instanceof URLClassLoader) { for (URL url : ((URLClassLoader) loader).getURLs()) { System.out.println(url); } } System.out.println(""); loader = loader.getParent(); } } }
結果は以下の様になります。
sun.misc.Launcher$AppClassLoader@4e25154f // -cp オプションもしくは CLASSPATH 環境変数で指定されたパスの一覧 sun.misc.Launcher$ExtClassLoader@33909752 // JAVA_HOME/jre/lib/ext もしくは JRE_HOME/lib/ext 内のパスの一覧
Class.forName(String className) の OpenJDK 実装を見てみると sun.reflect.Reflection クラスを使用して呼び出し元のクラスのクラスローダを取得しているようなので、AppClassLoader が使われているっぽい。
クラスローダの仕様で、まず親から探すらしいので ブートストラップクラスローダー > ExtClassLoader > AppClassLoader の順でクラスが検索されているようです。
Groovy のクラスローダ
Groovy 上でもクラスローダのオブジェクトを取得してみる。
Java の時はせいぜい2階層だったけど、Groovy は取得の仕方によってクラスローダの階層が若干異なります。
def showLoader(loader) { def indent = 0 while (loader != null) { print ' ' * (++indent * 2) println loader loader = loader.parent } println '' } class Test {} def localObj = new Test() println 'Class.classLoader' showLoader Class.classLoader println 'ClassLoader.systemClassLoader' showLoader ClassLoader.systemClassLoader println 'localObj.class.classLoader.rootLoader' showLoader localObj.class.classLoader.rootLoader println 'Thread.currentThread().contextClassLoader' showLoader Thread.currentThread().contextClassLoader println 'localObj.class.classLoader' showLoader localObj.class.classLoader
結果は以下の様になります。
Class.classLoader // 出力なし = ブートストラップクラスローダー ClassLoader.systemClassLoader sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@5e853265 localObj.class.classLoader.rootLoader org.codehaus.groovy.tools.RootLoader@2b193f2d sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@5e853265 Thread.currentThread().contextClassLoader groovy.lang.GroovyClassLoader@4441d8d9 org.codehaus.groovy.tools.RootLoader@2b193f2d sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@5e853265 localObj.class.classLoader groovy.lang.GroovyClassLoader$InnerLoader@4a694160 groovy.lang.GroovyClassLoader@4441d8d9 org.codehaus.groovy.tools.RootLoader@2b193f2d sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@5e853265
クラスパスも出力
def showClassPaths(loader) { while (loader != null) { println loader println loader.getURLs() println '' loader = loader.parent } println '' } class Test {} def localObj = new Test() showClassPaths localObj.class.classLoader
結果は以下の様になります。
groovy.lang.GroovyClassLoader$InnerLoader@13486a8a // なし groovy.lang.GroovyClassLoader@151cdf23 // なし org.codehaus.groovy.tools.RootLoader@2b193f2d // -cp オプションもしくは CLASSPATH 環境変数で指定されたパスの一覧 // GROOVY_HOME/lib 内のパスの一覧 sun.misc.Launcher$AppClassLoader@55f96302 // GROOVY_HOME/lib/groovy-x.x.x.jar(のみ) sun.misc.Launcher$ExtClassLoader@1f554b06 // JAVA_HOME/jre/lib/ext もしくは JRE_HOME/lib/ext 内のパスの一覧
Java の時は AppClassLoader に読み込まれていた CLASSPATH がすべて RootLoader に読まれています。
Groovy 起動時のクラスパスは Groovy のクラスローダに渡され、Groovy 自体を動作させる Java のクラスパスは groovy-x.x.x.jar が固定で指定されています。これは groovy を起動する startGroovy スクリプトで確認できます。
Groovy のクラスローダ (Grab 時)
今まで Grab を使ってガッツリコードを書いたことが無かったので知らなかったのですが、Grab を使う際にも、クラスローダを意識する必要があるらしいです。
@Grab(group='mysql', module='mysql-connector-java', version='x.x.xx') // JDBC ドライバを使ったコード ~~
上記のように書くだけだと JDBC ドライバが探し出せない様です。
上記のコードでロードされたクラスの位置は以下の様になってます。
groovy.lang.GroovyClassLoader$InnerLoader@13486a8a // Grab で指定したクラスパス groovy.lang.GroovyClassLoader@151cdf23 // Grab で指定したクラスパス org.codehaus.groovy.tools.RootLoader@2b193f2d sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@1f554b06
GroovyClassLoader と GroovyClassLoader$InnerLoader に読み込まれていますが、システムクラスローダに読み込ませてあげないとダメらしいので、Grub では GrabConfig アノテーションが用意されています。
@GrabConfig(systemClassLoader=true) @Grab(group='mysql', module='mysql-connector-java', version='x.x.xx') // JDBC ドライバを使ったコード ~~
GrabConfig を付けた後にもう一度クラスパスを表示すると
groovy.lang.GroovyClassLoader$InnerLoader@13486a8a // Grab で指定したクラスパス groovy.lang.GroovyClassLoader@151cdf23 // Grab で指定したクラスパス org.codehaus.groovy.tools.RootLoader@2b193f2d // Grab で指定したクラスパス (NEW!) sun.misc.Launcher$AppClassLoader@55f96302 sun.misc.Launcher$ExtClassLoader@1f554b06
RootLoader にクラスパスが追加されました。Groovy のシステムクラスローダが RootLoader というのも今始めて知りました。
じゃあ ClassLoader.systemClassLoader で取得できるのは何なんだ?って感じなんですが、これは Java のシステムクラスローダで Groovy のシステムクラスローダとは別物ということで納得するしかなさそう。
今回はまったのがまさにここで、java.net.URL クラスに独自プロトコルを認識させるため Handler クラスを読み込ませたいのに、 OpenJDK の実装を見る限り内部で ClassLoader.getSystemClassLoader() を呼び出しているので Java 上では問題なく動くのに Groovy 上で実行すると、自作のライブラリを全然認識してくれないんですよね(´Д⊂ヽ
今回調べたクラスローダに関しては以上です。