HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

ClassLoaderの"A sample implementation"

java.lang.ClassLoaderクラスに関するAPIドキュメントに書いてある"A sample implementation"をそのまま実装したようなクラスローダーが目の前にあって、「ふざけんなっ!」って思ったので、サンプルを使って再現してみよう(謎)。

まず、ClassLoaderの実装。

// エラー処理は一切していないので注意!!!
class SimpleClassLoader extends ClassLoader {
    private File basedir;
    public SimpleClassLoader(File basedir) {
        super(Thread.currentThread().getContextClassLoader());
        this.basedir = basedir;
    }
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        // 何かが足りない‥(謎)
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) throws ClassNotFoundException {
        try {
            String fileName = name.replace('.', File.separatorChar) + ".class";
            File file = new File(basedir, fileName);
            // 以下、このファイルからバイト列を読み込んで配列にして返す。

次に、このクラスローダーを使ってロードするクラス。

package sample.class_loader;

public class Hoge {
    public Hoge() {
        System.out.println("class-loader: " + getClass().getClassLoader());
        System.out.println("class-name: " + getClass().getName());
        System.out.println("package-name: " + getClass().getPackage().getName());
    }
}

作ったクラスローダでHogeクラスをロードする処理を行うメソッド

import java.io.File;

public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println("java.version=" + System.getProperty("java.version"));
        ClassLoader cl = new SimpleClassLoader(new File("c2"));
        Class cls = Class.forName("sample.class_loader.Hoge", true, cl);
        Object obj = cls.newInstance();
    }
}

で、Hogeクラスだけ別のクラスパスに配置するようにするために、コンパイル手順は以下のようにする。

mkdir c1 c2
javac -d c1 SimpleClassLoader.java Main.java
javac -d c2 Hoge.java
java -classpath "c1" Main

で、実行すると、こんな感じ。

java.version=1.6.0_02
class-loader: SimpleClassLoader@1a758cb
class-name: sample.class_loader.Hoge
Exception in thread "main" java.lang.NullPointerException
        at sample.class_loader.Hoge.<init>(Hoge.java:7)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
        at java.lang.Class.newInstance0(Class.java:355)
        at java.lang.Class.newInstance(Class.java:308)
        at Main.main(Main.java:8)

なぜかというと、冒頭のSimpleClassLoaderのfindClassメソッドにおいて「何かが足りない‥(謎)」と書いた部分に必要な処理が抜けている。

java.lang.ClassLoaderクラスのdefinePackageメソッドに関するJavaDoc*1を読むと、以下のように書いてある。

Defines a package by name in this ClassLoader. This allows class loaders to define the packages for their classes. Packages must be created before the class is defined, and package names must be unique within a class loader and cannot be redefined or changed once created.

つまり、defineClassメソッドを呼ぶ前に、こんな感じの処理が必要になる。

    int idx = name.lastIndexOf(".");
    if (idx > 0) {
        String pkgName = name.substring(0, idx);
        Package pkg = getPackage(pkgName);
        if (pkg == null) {
            definePackage(pkgName, null, null, null, null, null, null, null);
        }
    }

まぁ、クラスの動的ロードを可能にするような仕組みとかを作りたくてクラスローダーを自作することはあるんだろけど、単に「起動時のクラスパスには含まれていない特別な場所からクラスをロードしたい」というような場合、java.net.URLClassLoaderを使うほうがよっぽど楽で安全。

URL[] urls = new URL[] {
    new File("c2").toURI().toURL(),
};
ClassLoader cl = new URLClassLoader(urls);

まぁ、この場合、親クラスローダーの方が優先されてしまうので、子クラスローダーを優先したい場合は細工が必要なんだろうけど‥


あー、疲れた(謎)‥