Java ClassLoader

Java ClassLoaderは、プロジェクト開発において重要な要素の1つですが、あまり使われることはありません。私はこれまでのプロジェクトのどれにもClassLoaderを拡張したことがありません。しかし、Javaクラスのローディングをカスタマイズできる独自のClassLoaderを持つというアイデアは興味深いものです。この記事では、Java ClassLoaderの概要を提供し、その後Javaでカスタムクラスローダーを作成する方法について説明します。

Java ClassLoaderとは何ですか?

JavaプログラムがJava仮想マシン(JVM)上で実行されることを知っています。Javaクラスをコンパイルすると、JVMはプラットフォームやマシンに依存しないバイトコードを生成します。このバイトコードは、.classファイルに保存されます。クラスを使用しようとすると、ClassLoaderがメモリにロードします。

組み込みのClassLoaderの種類

Javaには3種類の組み込みのClassLoaderがあります。

  1. Bootstrap Class Loader – JDKの内部クラスをロードします。rt.jarやjava.lang.*パッケージの他のコアクラスなどです。
  2. Extensions Class Loader – JDK拡張ディレクトリからクラスをロードします。通常は$JAVA_HOME/lib/extディレクトリです。
  3. システムクラスローダー – このクラスローダーは現在のクラスパスからクラスをロードします。プログラムを起動する際には、-cpまたは-classpathコマンドラインオプションを使用してクラスパスを設定できます。

クラスローダーの階層

クラスローダーはクラスをメモリにロードする際に階層的です。クラスをロードする要求があると、それは親クラスローダーに委任されます。これにより実行環境で一意性が維持されます。親クラスローダーがクラスを見つけられない場合、クラスローダー自体がクラスをロードしようとします。これを以下のJavaプログラムを実行して理解しましょう。

package com.journaldev.classloader;

public class ClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("class loader for HashMap: "
                + java.util.HashMap.class.getClassLoader());
        System.out.println("class loader for DNSNameService: "
                + sun.net.spi.nameservice.dns.DNSNameService.class
                        .getClassLoader());
        System.out.println("class loader for this class: "
                + ClassLoaderTest.class.getClassLoader());

        System.out.println(com.mysql.jdbc.Blob.class.getClassLoader());

    }

}

出力:

class loader for HashMap: null
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@7c354093
class loader for this class: sun.misc.Launcher$AppClassLoader@64cbbe37
sun.misc.Launcher$AppClassLoader@64cbbe37

Java ClassLoaderの動作は?

上記のプログラムの出力からクラスローダーの動作を理解しましょう。

  • java.util.HashMapクラスローダーがnullとして表示され、これはBootstrap ClassLoaderを反映しています。DNSNameServiceクラスのクラスローダーはExtClassLoaderです。クラス自体がCLASSPATHにあるため、System ClassLoaderがそれをロードします。
  • 当HashMapをロードしようとすると、System ClassLoaderはそれをExtension ClassLoaderに委任します。拡張クラスローダーはBootstrap ClassLoaderにそれを委任します。ブートストラップクラスローダーはHashMapクラスを見つけ、JVMメモリにロードします。
  • DNSNameServiceクラスに対しても同じプロセスが適用されます。ただし、ブートストラップクラスローダーは$JAVA_HOME/lib/ext/dnsns.jar内にあるため、それを見つけることができません。そのため、Extensions Classloaderによってロードされます。
  • Blobクラスは、MySql JDBC Connector jar(mysql-connector-java-5.0.7-bin.jar)に含まれており、プロジェクトのビルドパスに存在しています。これもSystem Classloaderによってロードされます。
  • 子クラスローダーによってロードされたクラスは、その親クラスローダーによってロードされたクラスが見えます。したがって、System ClassloaderによってロードされたクラスはExtensionsおよびBootstrap Classloaderによってロードされたクラスが見えます。
  • 兄弟クラスローダーが存在する場合、お互いにロードされたクラスにアクセスできません。

なぜJavaでカスタムクラスローダーを作成するのか?

Javaのデフォルトのクラスローダーは、ほとんどのケースでローカルファイルシステムからクラスをロードできます。ただし、実行時またはFTPサーバー、またはクラスをロードする際にサードパーティのWebサービスからクラスを期待している場合は、既存のクラスローダーを拡張する必要があります。たとえば、AppletViewersはリモートWebサーバーからクラスをロードします。

Java ClassLoaderメソッド

  • クラスが要求されると、JVMはClassLoaderのloadClass()関数を呼び出し、クラスの完全修飾名を渡します。
  • loadClass()関数はfindLoadedClass()メソッドを呼び出してクラスが既にロードされているかどうかを確認します。同じクラスを複数回ロードしないようにするために必要です。
  • クラスがまだロードされていない場合は、リクエストを親ClassLoaderに委任してクラスをロードします。
  • 親ClassLoaderがクラスを見つけられない場合は、findClass()メソッドを呼び出してファイルシステムでクラスを検索します。

JavaカスタムClassLoaderの例

私たちは、ClassLoaderクラスを拡張し、loadClass(String name)メソッドをオーバーライドして独自のClassLoaderを作成します。クラス名がcom.journaldevで始まる場合は、カスタムクラスローダーを使用してそれをロードし、それ以外の場合は親のClassLoader loadClass()メソッドを呼び出してクラスをロードします。

1. CCLoader.java

以下は、当社の独自のクラスローダーに含まれるメソッドです。

  1. private byte[] loadClassFileData(String name): このメソッドは、ファイルシステムからクラスファイルをバイト配列に読み込みます。
  2. private Class<?> getClass(String name): このメソッドは、loadClassFileData()関数を呼び出し、親のdefineClass()メソッドを起動してクラスを生成し、それを返します。
  3. public Class<?> loadClass(String name): このメソッドは、クラスをロードする責任があります。クラス名がcom.journaldevで始まる場合(当社のサンプルクラス)、getClass()メソッドを使用してそれをロードします。それ以外の場合は、親のloadClass()関数を呼び出してそれをロードします。
  4. public CCLoader(ClassLoader parent): これはコンストラクタであり、親のClassLoaderを設定する責任があります。
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
/**
 * Our Custom ClassLoader to load the classes. Any class in the com.journaldev
 * package will be loaded using this ClassLoader. For other classes, it will delegate the request to its Parent ClassLoader.
 *
 */
public class CCLoader extends ClassLoader {
 
    /**
     * This constructor is used to set the parent ClassLoader
     */
    public CCLoader(ClassLoader parent) {
        super(parent);
    }
 
    /**
     * Loads the class from the file system. The class file should be located in
     * the file system. The name should be relative to get the file location
     *
     * @param name
     *            Fully Classified name of the class, for example, com.journaldev.Foo
     */
    private Class getClass(String name) throws ClassNotFoundException {
        String file = name.replace('.', File.separatorChar) + ".class";
        byte[] b = null;
        try {
            // このコードはファイルからバイトコードデータを読み込みます
            b = loadClassFileData(file);
            // defineClassはClassLoaderクラスから継承されたもので、バイト配列をクラスに変換します。 defineClassはFinalなので、オーバーライドできません
            // 
            これはTestClassです。
            Class c = defineClass(name, b, 0, b.length);
            resolveClass(c);
            return c;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * Every request for a class passes through this method. If the class is in
     * com.journaldev package, we will use this classloader or else delegate the
     * request to parent classloader.
     *
     *
     * @param name
     *            Full class name
     */
    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading Class '" + name + "'");
        if (name.startsWith("com.journaldev")) {
            System.out.println("Loading Class using CCLoader");
            return getClass(name);
        }
        return super.loadClass(name);
    }
 
    /**
     * Reads the file (.class) into a byte array. The file should be
     * accessible as a resource and make sure that it's not in Classpath to avoid
     * any confusion.
     *
     * @param name
     *            Filename
     * @return Byte array read from the file
     * @throws IOException
     *             if an exception comes in reading the file
     */
    private byte[] loadClassFileData(String name) throws IOException {
        InputStream stream = getClass().getClassLoader().getResourceAsStream(
                name);
        int size = stream.available();
        byte buff[] = new byte[size];
        DataInputStream in = new DataInputStream(stream);
        in.readFully(buff);
        in.close();
        return buff;
    }
}

2. CCRun.java

これはmain functionを持つTestClassです。クラスローダーのインスタンスを作成し、そのloadClass()メソッドを使用してサンプルクラスを読み込んでいます。クラスを読み込んだ後、Java Reflection APIを使用してそのメソッドを呼び出しています。

import java.lang.reflect.Method;
 
public class CCRun {
 
    public static void main(String args[]) throws Exception {
        String progClass = args[0];
        String progArgs[] = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);

        CCLoader ccl = new CCLoader(CCRun.class.getClassLoader());
        Class clas = ccl.loadClass(progClass);
        Class mainArgType[] = { (new String[0]).getClass() };
        Method main = clas.getMethod("main", mainArgType);
        Object argsArray[] = { progArgs };
        main.invoke(null, argsArray);

        // 以下のメソッドは、Fooがカスタムクラスローダーで読み込まれているかどうかを確認するために使用されます
        // 
        Method printCL = clas.getMethod("printCL", null);
        printCL.invoke(null, new Object[0]);
    }
 
}

3. Foo.javaとBar.java

これらはカスタムクラスローダーによって読み込まれるTestClassです。彼らはprintCL()メソッドを持っており、これはClassLoader情報を印刷するために呼び出されています。Fooクラスはカスタムクラスローダーによって読み込まれます。FooはBarクラスを使用しているため、Barクラスもカスタムクラスローダーによって読み込まれます。

package com.journaldev.cl;
 
public class Foo {
    static public void main(String args[]) throws Exception {
        System.out.println("Foo Constructor >>> " + args[0] + " " + args[1]);
        Bar bar = new Bar(args[0], args[1]);
        bar.printCL();
    }
 
    public static void printCL() {
        System.out.println("Foo ClassLoader: "+Foo.class.getClassLoader());
    }
}
package com.journaldev.cl;
 
public class Bar {
 
    public Bar(String a, String b) {
        System.out.println("Bar Constructor >>> " + a + " " + b);
    }
 
    public void printCL() {
        System.out.println("Bar ClassLoader: "+Bar.class.getClassLoader());
    }
}

4. Java カスタム ClassLoader 実行手順

まず、すべてのクラスをコマンドラインでコンパイルします。その後、CCRun クラスを実行し、三つの引数を渡します。最初の引数は、クラスローダーによってロードされる Foo クラスの完全修飾名です。残りの二つの引数は、Foo クラスの main 関数と Bar コンストラクタに渡されます。実行手順と出力は以下のようになります。

$ javac -cp . com/journaldev/cl/Foo.java
$ javac -cp . com/journaldev/cl/Bar.java
$ javac CCLoader.java
$ javac CCRun.java
CCRun.java:18: warning: non-varargs call of varargs method with inexact argument type for last parameter;
cast to java.lang.Class<?> for a varargs call
cast to java.lang.Class<?>[] for a non-varargs call and to suppress this warning
Method printCL = clas.getMethod("printCL", null);
^
1 warning
$ java CCRun com.journaldev.cl.Foo 1212 1313
Loading Class 'com.journaldev.cl.Foo'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.Exception'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.io.PrintStream'
Foo Constructor >>> 1212 1313
Loading Class 'com.journaldev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$

出力を見ると、com.journaldev.cl.Foo クラスをロードしようとしています。これは java.lang.Object クラスを拡張しているため、まず Object クラスをロードしようとしています。そのため、要求は CCLoader の loadClass メソッドに来ており、それは親クラスに委譲しています。親クラスローダーがObject、String、および他のJavaクラスをロードしています。当社のクラスローダーは、ファイルシステムからFooおよびBarクラスのみをロードしています。printCL() 関数の出力から明確です。loadClassFileData() の機能を変更して、クラスのバイト配列をFTPサーバーから読み取るか、クラスのバイト配列を動的に取得するためにサードパーティサービスを呼び出すことができます。この記事がJava ClassLoaderの動作を理解し、ファイルシステムから取得するだけでなく、さまざまなことができるように拡張する方法に役立つことを願っています。

カスタムClassLoaderをデフォルトのClassLoaderとして作成する

Javaオプションを使用して、JVMが起動するときに独自のクラスローダーをデフォルトにすることができます。例えば、javaクラスローダーオプションを指定してから、ClassLoaderTestプログラムをもう一度実行します。

$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/journaldev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.journaldev.classloader.ClassLoaderTest
Loading Class 'com.journaldev.classloader.ClassLoaderTest'
Loading Class using CCLoader
Loading Class 'java.lang.Object'
Loading Class 'java.lang.String'
Loading Class 'java.lang.System'
Loading Class 'java.lang.StringBuilder'
Loading Class 'java.util.HashMap'
Loading Class 'java.lang.Class'
Loading Class 'java.io.PrintStream'
class loader for HashMap: null
Loading Class 'sun.net.spi.nameservice.dns.DNSNameService'
class loader for DNSNameService: sun.misc.Launcher$ExtClassLoader@24480457
class loader for this class: CCLoader@38503429
Loading Class 'com.mysql.jdbc.Blob'
sun.misc.Launcher$AppClassLoader@2f94ca6c
$

CCLoaderは、com.journaldevパッケージ内にあるため、ClassLoaderTestクラスをロードしています。

クラスローダーの例のコードは、GitHubリポジトリからダウンロードできます。

Source:
https://www.digitalocean.com/community/tutorials/java-classloader