Javaクラスローダーの基本

Java仮想マシンの基礎の1つであるクラスローダーの概念は、名前付きクラスをそのクラスの実装を担当するビットに変換する動作を説明しています。クラスローダーが存在するため、Javaランタイムは、Javaプログラムを実行するときに、ファイルやファイルシステムについて何も知る必要はありません。

クラスローダーの機能

クラスは、すでに実行されているクラスで名前によって参照されるときにJava環境に導入されます。最初のクラスを実行するために続く魔法が少しありますが(そのため、main()メソッドを静的として宣言し、文字列配列を引数として使用する必要があります)、そのクラスが実行されると、将来はクラスのロードは、クラスローダーによって行われます。

最も単純なクラスローダーは、文字列名で参照されるクラス本体のフラットな名前空間を作成します。メソッドの定義は次のとおりです。

クラスr = loadClass(String className、boolean resolveIt); 

変数classNameには、クラスローダーによって理解され、クラス実装を一意に識別するために使用される文字列が含まれています。変数resolveItは、このクラス名で参照されているクラスを解決する必要があること(つまり、参照されているクラスもすべてロードする必要があること)をクラスローダーに通知するフラグです。

すべてのJava仮想マシンには、仮想マシンに組み込まれている1つのクラスローダーが含まれています。この組み込みローダーは、原始クラスローダーと呼ばれます。仮想マシンは、検証なしでVMによって実行できる信頼できるクラスのリポジトリにアクセスできると想定しているため、これはやや特別です。

原始クラスローダーは、loadClass()のデフォルトの実装を実装します。したがって、このコードは、クラス名java.lang.Objectが、クラスパスのどこかに接頭辞java / lang /Object.classが付いたファイルに格納されていることを理解しています。このコードは、クラスパス検索とクラスのzipファイルの検索の両方を実装します。これが設計されている方法の本当にすばらしい点は、Javaがクラスローダーを実装する関数のセットを変更するだけでクラスストレージモデルを変更できることです。

Java仮想マシンの本質を掘り下げてみると、原始的なクラスローダーが主に関数FindClassFromClassResolveClassに実装されていることがわかります

では、クラスはいつロードされますか?2例が正確にあります。新しいバイトコードが実行されたときに(例えば、FooClassのF =新しいFooClass() ;)と、バイトコードは、クラスに静的な参照を行う場合(例えば、システム。アウト)。

非原始的なクラスローダー

"だから何?" あなたは尋ねるかもしれません。

Java仮想マシンにはフックがあり、基本的なクラスローダーの代わりにユーザー定義のクラスローダーを使用できます。さらに、ユーザークラスローダーはクラス名を最初にクラックするため、ユーザーは任意の数の興味深いクラスリポジトリを実装できます。その中でも、HTTPサーバーは、そもそもJavaを軌道に乗せたものです。

ただし、クラスローダーは非常に強力であるため(たとえば、java.lang.Objectを独自のバージョンに置き換えることができる)、アプレットなどのJavaクラスで独自のローダーをインスタンス化することはできません。(ちなみに、これはクラスローダーによって強制されます。)この列は、信頼できるクラスリポジトリから実行されているアプリケーション(ローカルファイルなど)でのみ、アプレットを使用してこのようなことを実行しようとしている場合には役立ちません。

ユーザークラスローダーは、基本的なクラスローダーよりも先にクラスをロードする機会を得ます。このため、代替ソースからクラス実装データをロードできます。これは、AppletClassLoaderがHTTPプロトコルを使用してクラスをロードする方法です。

SimpleClassLoaderの構築

クラスローダーは、java.lang.ClassLoaderのサブクラスになることから始まります。実装する必要がある唯一の抽象メソッドはloadClass()です。loadClass()のフローは次のとおりです。

  • クラス名を確認します。
  • 要求されたクラスがすでにロードされているかどうかを確認してください。
  • クラスが「システム」クラスであるかどうかを確認してください。
  • このクラスローダーのリポジトリからクラスをフェッチしてみてください。
  • VMのクラスを定義します。
  • クラスを解決します。
  • クラスを呼び出し元に返します。

SimpleClassLoaderは次のように表示され、コードに何が散在しているかについての説明があります。

パブリック同期クラスloadClass(String className、boolean resolveIt)throws ClassNotFoundException {クラス結果;バイトclassData []; System.out.println( ">>>>>>クラスのロード:" + className); / *クラスのローカルキャッシュを確認します* / result =(Class)classes.get(className); if(result!= null){System.out.println( ">>>>>>キャッシュされた結果を返します。");結果を返します。 }

上記のコードは、loadClassメソッドの最初のセクションです。ご覧のとおり、クラス名を取得し、クラスローダーがすでに返しているクラスを維持しているローカルハッシュテーブルを検索します。要求されるたびに同じクラス名に対して同じクラスオブジェクト参照を返す必要があるため、このハッシュテーブルを保持することが重要です。そうしないと、システムは同じ名前の2つの異なるクラスがあると見なし、それらの間にオブジェクト参照を割り当てるたびにClassCastExceptionをスローしますloadClass()はキャッシュを保持することも重要です。 メソッドは、クラスが解決されているときに再帰的に呼び出され、別のコピーのために追跡するのではなく、キャッシュされた結果を返す必要があります。

/ *原始クラスローダーで確認します* / try {result = super.findSystemClass(className); System.out.println( ">>>>>>システムクラスを返す(CLASSPATH内)。");結果を返します。 } catch(ClassNotFoundException e){System.out.println( ">>>>>>システムクラスではありません。"); }

上記のコードでわかるように、次のステップは、原始クラスローダーがこのクラス名を解決できるかどうかを確認することです。このチェックは、システムの健全性とセキュリティの両方に不可欠です。たとえば、java.lang.Objectの独自のインスタンスを呼び出し元に返す場合、このオブジェクトは他のオブジェクトと共通のスーパークラスを共有しません。クラスローダーが独自の値java.lang.SecurityManagerを返した場合、システムのセキュリティが危険にさらされる可能性があります。この値には、実際のチェックと同じチェックがありませんでした。

/ *リポジトリからロードしてみてください* / classData = getClassImplFromDataBase(className); if(classData == null){throw new ClassNotFoundException(); }

After the initial checks, we come to the code above which is where the simple class loader gets an opportunity to load an implementation of this class. The SimpleClassLoader has a method getClassImplFromDataBase() which in our simple example merely prefixes the directory "store\" to the class name and appends the extension ".impl". I chose this technique in the example so that there would be no question of the primordial class loader finding our class. Note that the sun.applet.AppletClassLoader prefixes the codebase URL from the HTML page where an applet lives to the name and then does an HTTP get request to fetch the bytecodes.

 /* Define it (parse the class file) */ result = defineClass(classData, 0, classData.length); 

クラス実装がロードされた場合、最後から2番目のステップはjava.lang.ClassLoaderからdefineClass()メソッドを呼び出すことです。これは、クラス検証の最初のステップと見なすことができます。このメソッドはJava仮想マシンに実装されており、クラスバイトが正当なJavaクラスファイルであることを確認する役割を果たします。内部的には、defineClassメソッドは、JVMがクラスを保持するために使用するデータ構造に入力します。クラスデータの形式が正しくない場合、この呼び出しによりClassFormatErrorがスローされます。

if(resolveIt){resolveClass(result); }

最後のクラスローダー固有の要件は、ブールパラメータresolveItがtrueの場合にresolveClass()を呼び出すことです。このメソッドは2つのことを行います。1つは、このクラスによって明示的に参照されるすべてのクラスがロードされ、このクラスのプロトタイプオブジェクトが作成されることです。次に、ベリファイアを呼び出して、このクラスのバイトコードの正当性を動的に検証します。検証が失敗した場合、このメソッド呼び出しはLinkageErrorをスローします。最も一般的なのはVerifyErrorです。

Note that for any class you will load, the resolveIt variable will always be true. It is only when the system is recursively calling loadClass() that it may set this variable false because it knows the class it is asking for is already resolved.

 classes.put(className, result); System.out.println(" >>>>>> Returning newly loaded class."); return result; } 

The final step in the process is to store the class we've loaded and resolved into our hash table so that we can return it again if need be, and then to return the Class reference to the caller.

Of course if it were this simple there wouldn't be much more to talk about. In fact, there are two issues that class loader builders will have to deal with, security and talking to classes loaded by the custom class loader.

Security considerations

Whenever you have an application loading arbitrary classes into the system through your class loader, your application's integrity is at risk. This is due to the power of the class loader. Let's take a moment to look at one of the ways a potential villain could break into your application if you aren't careful.

In our simple class loader, if the primordial class loader couldn't find the class, we loaded it from our private repository. What happens when that repository contains the class java.lang.FooBar ? There is no class named java.lang.FooBar, but we could install one by loading it from the class repository. This class, by virtue of the fact that it would have access to any package-protected variable in the java.lang package, can manipulate some sensitive variables so that later classes could subvert security measures. Therefore, one of the jobs of any class loader is to protect the system name space.

In our simple class loader we can add the code:

 if (className.startsWith("java.")) throw newClassNotFoundException(); 

just after the call to findSystemClass above. This technique can be used to protect any package where you are sure that the loaded code will never have a reason to load a new class into some package.

Another area of risk is that the name passed must be a verified valid name. Consider a hostile application that used a class name of "..\..\..\..\netscape\temp\xxx.class" as its class name that it wanted loaded. Clearly, if the class loader simply presented this name to our simplistic file system loader this might load a class that actually wasn't expected by our application. Thus, before searching our own repository of classes, it is a good idea to write a method that verifies the integrity of your class names. Then call that method just before you go to search your repository.

Using an interface to bridge the gap

The second non-intuitive issue with working with class loaders is the inability to cast an object that was created from a loaded class into its original class. You need to cast the object returned because the typical use of a custom class loader is something like:

 CustomClassLoader ccl = new CustomClassLoader(); Object o; Class c; c = ccl.loadClass("someNewClass"); o = c.newInstance(); ((SomeNewClass)o).someClassMethod(); 

However, you cannot cast o to SomeNewClass because only the custom class loader "knows" about the new class it has just loaded.

There are two reasons for this. First, the classes in the Java virtual machine are considered castable if they have at least one common class pointer. However, classes loaded by two different class loaders will have two different class pointers and no classes in common (except java.lang.Object usually). Second, the idea behind having a custom class loader is to load classes after the application is deployed so the application does not know a priory about the classes it will load. This dilemma is solved by giving both the application and the loaded class a class in common.

There are two ways of creating this common class, either the loaded class must be a subclass of a class that the application has loaded from its trusted repository, or the loaded class must implement an interface that was loaded from the trusted repository. This way the loaded class and the class that does not share the complete name space of the custom class loader have a class in common. In the example I use an interface named LocalModule, although you could just as easily make this a class and subclass it.

最初の手法の最良の例はWebブラウザです。すべてのアプレットによって実装されるJavaによって定義されたクラスは、java.applet.Appletです。クラスがAppletClassLoaderによってロードされると、作成されたオブジェクトインスタンスがアプレットのインスタンスにキャストされます。このキャストが成功すると、init()メソッドが呼び出されます。私の例では、2番目の手法であるインターフェースを使用します。

例で遊ぶ

例を締めくくるために、私はさらにいくつか作成しました

.java

ファイル。これらは:

public interface LocalModule {/ *モジュールを開始します* / void start(String option); }