Java で作ったツールを Groovy 上で動かそうとした時に ClassLoader 回りではまってしまったのでメモ
Java のクラスローダについては以下の通り。
Javaクラスローダー - Wikipedia
まず 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 ドライバが探し出せない様です。
上記のコードでロードされたクラスの位置は以下の様になってます。
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')
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 上で実行すると、自作のライブラリを全然認識してくれないんですよね(´Д⊂ヽ
今回調べたクラスローダに関しては以上です。