Javaのクラスローダー
JavaのClassLoaderは、プロジェクト開発において重要ながらもあまり使用されないコンポーネントの一つです。私は今までのプロジェクトでClassLoaderを拡張したことはありませんが、Javaのクラスのローディングをカスタマイズできる独自のClassLoaderを持つというアイディアは魅力的です。この記事では、JavaのClassLoaderの概要を提供し、その後Javaでカスタムクラスローダーを作成する方法を紹介します。
Java ClassLoaderとは何ですか?
私たちはJavaプログラムがJava仮想マシン(JVM)上で実行されることを知っています。Javaクラスをコンパイルすると、JVMはプラットフォームやマシンに依存しないバイトコードを作成します。このバイトコードは、.classファイルに保存されます。クラスを使用しようとすると、クラスローダーがメモリに読み込みます。
組み込みのClassLoaderの種類
Javaには3つの組み込みクラスローダーのタイプがあります。
-
- ブートストラップクラスローダーは、JDKの内部クラスを読み込みます。例えば、java.lang.* パッケージのクラスなど、rt.jarや他のコアクラスを読み込みます。
-
- 拡張クラスローダーは、通常$JAVA_HOME/lib/extディレクトリからJDKの拡張機能のクラスを読み込みます。
- システムクラスローダーは、現在のクラスパスからクラスを読み込みます。-cpまたは-classpathコマンドラインオプションを使用してプログラムを起動する際にクラスパスを設定することができます。
クラスローダーの階層
クラスローダーは、クラスをメモリにロードする際に階層的になります。クラスのロードリクエストが発生すると、親クラスローダーに委任します。これにより、実行環境でユニークさが維持されます。もし親クラスローダーがクラスを見つけられない場合、クラスローダー自体がクラスをロードしようとします。以下のJavaプログラムを実行することで、これを理解しましょう。
package com.scdev.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のクラスローダーはどのように動作するのですか?
上記のプログラムの出力から、クラスローダーの動作を理解しましょう。
- The java.util.HashMap ClassLoader is coming as null, which reflects Bootstrap ClassLoader. The DNSNameService class ClassLoader is ExtClassLoader. Since the class itself is in CLASSPATH, System ClassLoader loads it.
- When we are trying to load HashMap, our System ClassLoader delegates it to the Extension ClassLoader. The extension class loader delegates it to the Bootstrap ClassLoader. The bootstrap class loader finds the HashMap class and loads it into the JVM memory.
- The same process is followed for the DNSNameService class. But, the Bootstrap ClassLoader is not able to locate it since it’s in $JAVA_HOME/lib/ext/dnsns.jar. Hence, it gets loaded by Extensions Classloader.
- The Blob class is included in the MySql JDBC Connector jar (mysql-connector-java-5.0.7-bin.jar), which is present in the build path of the project. It’s also getting loaded by the System Classloader.
- The classes loaded by a child class loader have visibility into classes loaded by its parent class loaders. So classes loaded by System Classloader have visibility into classes loaded by Extensions and Bootstrap Classloader.
- If there are sibling class loaders then they can’t access classes loaded by each other.
なぜJavaでカスタムのClassLoaderを作成するのですか?
JavaのデフォルトのClassLoaderは、ほとんどの場合にはローカルのファイルシステムからクラスを読み込むことができます。しかし、実行時やFTPサーバーや第三者のWebサービスからクラスを読み込む必要がある場合は、既存のクラスローダーを拡張する必要があります。例えば、AppletViewersはリモートのWebサーバーからクラスを読み込みます。
Javaのクラスローダーメソッドを日本語で言い換えると、「Javaのクラスローダーメソッド」となります。
- When JVM requests for a class, it invokes loadClass() function of the ClassLoader by passing the fully classified name of the Class.
- The loadClass() function calls the findLoadedClass() method to check that the class has been already loaded or not. It’s required to avoid loading the same class multiple times.
- If the Class is not already loaded, then it will delegate the request to parent ClassLoader to load the class.
- If the parent ClassLoader doesn’t find the class then it will invoke findClass() method to look for the classes in the file system.
Javaのカスタムクラスローダーの例とは、異なる場所からクラスをロードするためにカスタムクラスローダーを使用する方法を示したものです。
1. CCLoader.java を日本語で自然に言い換えると、次のようになります。
以下のメソッドを持つ私たちのカスタムクラスローダーです。
-
- nameという名前のメソッドは、ファイルシステムからクラスファイルをバイト配列に読み込みます。
-
- nameという名前のメソッドは、loadClassFileData()関数を呼び出し、親のdefineClass()メソッドを呼び出してClassを生成し、それを返します。
-
- nameという名前のメソッドは、クラスをロードする責任を持っています。クラス名がcom.scdev(サンプルクラス)で始まる場合、getClass()メソッドを使用してそれをロードします。そうでない場合は、親loadClass()関数を呼び出してそれをロードします。
- 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.scdev
* 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.scdev.Foo
*/
private Class getClass(String name) throws ClassNotFoundException {
String file = name.replace('.', File.separatorChar) + ".class";
byte[] b = null;
try {
// This loads the byte code data from the file
b = loadClassFileData(file);
// defineClass is inherited from the ClassLoader class
// that converts byte array into a Class. defineClass is Final
// so we cannot override it
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.scdev 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.scdev")) {
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を日本語で言い換える。
これは私たちのテストクラスで、メイン関数が含まれています。私たちはClassLoaderのインスタンスを作成し、そのloadClass()メソッドを使用してサンプルクラスを読み込んでいます。クラスを読み込んだ後、Javaのリフレクション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);
// Below method is used to check that the Foo is getting loaded
// by our custom class loader i.e CCLoader
Method printCL = clas.getMethod("printCL", null);
printCL.invoke(null, new Object[0]);
}
}
3. Foo.javaとBar.java
これらは、カスタムクラスローダーによってロードされる私たちのテストクラスです。それらにはprintCL()メソッドがあり、ClassLoader情報を表示するために呼び出されます。Fooクラスは私たちのカスタムクラスローダーによってロードされます。FooはBarクラスを使用するため、Barクラスも私たちのカスタムクラスローダーによってロードされます。
package com.scdev.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.scdev.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 カスタムクラスローダーの実行手順
まず、最初に、すべてのクラスをコマンドラインからコンパイルします。その後、CCRunクラスを3つの引数を渡して実行します。最初の引数は、クラスローダーによって読み込まれるFooクラスの完全分類された名前です。他の2つの引数は、Fooクラスのmain関数とBarコンストラクタに渡されます。実行手順と出力は以下のようになります。
$ javac -cp . com/scdev/cl/Foo.java
$ javac -cp . com/scdev/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.scdev.cl.Foo 1212 1313
Loading Class 'com.scdev.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.scdev.cl.Bar'
Loading Class using CCLoader
Bar Constructor >>> 1212 1313
Loading Class 'java.lang.Class'
Bar ClassLoader: CCLoader@71f6f0bf
Foo ClassLoader: CCLoader@71f6f0bf
$
出力を確認すると、com.scdev.cl.Fooクラスのロードを試みています。java.lang.Objectクラスを拡張しているため、まずObjectクラスのロードを試みています。そのため、要求はCCLoaderのloadClassメソッドに渡され、そこから親クラスに委譲されます。したがって、親クラスローダーがObjectクラス、Stringクラス、他のJavaクラスをロードしています。私たちのClassLoaderは、ファイルシステムからFooクラスとBarクラスのみをロードしています。printCL()関数の出力からは明らかです。loadClassFileData()の機能を変更して、クラスのバイト配列をFTPサーバーから読み込むか、クラスのバイト配列を動的に取得するためにサードパーティーサービスを呼び出すことも可能です。この記事がJava ClassLoaderの動作や、ファイルシステムからの取得だけでなく、それ以上のことを行うためにどのように拡張できるかを理解するのに役立てば幸いです。
デフォルトのClassLoaderとしてカスタムClassLoaderを作成する。
JVMが起動する際に、Javaオプションを使用して、カスタムクラスローダーをデフォルトに設定することができます。例えば、Javaクラスローダーオプションを指定してから、再度ClassLoaderTestプログラムを実行します。
$ javac -cp .:../lib/mysql-connector-java-5.0.7-bin.jar com/scdev/classloader/ClassLoaderTest.java
$ java -cp .:../lib/mysql-connector-java-5.0.7-bin.jar -Djava.system.class.loader=CCLoader com.scdev.classloader.ClassLoaderTest
Loading Class 'com.scdev.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.scdevパッケージにClass Loader Testクラスがあるため、それをロードしています。
当社のGitHubリポジトリから、ClassLoaderの例のコードをダウンロードすることができます。