Groovy の ClassLoader

Java で作ったツールを Groovy 上で動かそうとした時に ClassLoader 回りではまってしまったのでメモ

Java のクラスローダ

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 ドライバを使ったコード ~~

上記のように書くだけだと 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 上で実行すると、自作のライブラリを全然認識してくれないんですよね(´Д⊂ヽ

今回調べたクラスローダに関しては以上です。