Основите на зареждачите от клас Java

Концепцията за зареждане на клас, един от крайъгълните камъни на виртуалната машина Java, описва поведението на преобразуването на именен клас в битовете, отговорни за внедряването на този клас. Тъй като зареждачите на класове съществуват, времето за изпълнение на Java не трябва да знае нищо за файловете и файловите системи при изпълнение на Java програми.

Какво правят класните товарачи

Класовете се въвеждат в Java средата, когато към тях се препраща по име в клас, който вече се изпълнява. Има малко магия, която продължава, за да стартира първият клас (поради което трябва да декларирате метода main () като статичен, като вземете масив от низове като аргумент), но след като този клас се изпълни, бъдещи опити за класовете за зареждане се извършват от зареждащия клас.

В най-простия случай, зареждащият клас създава плоско пространство от имена на тела на класа, които са посочени от име на низ. Определението на метода е:

Клас r ​​= loadClass (String className, boolean resolIt); 

Променливата className съдържа низ, който се разбира от зареждащия клас и се използва за уникално идентифициране на изпълнение на клас. Променливата resolIt е флаг, който казва на зареждащия клас, че класовете, посочени от това име на клас, трябва да бъдат разрешени (т.е. всеки рефериран клас също трябва да бъде зареден).

Всички Java виртуални машини включват един клас товарач, който е вграден във виртуалната машина. Този вграден товарач се нарича първоначален клас товарач. Това е донякъде специално, тъй като виртуалната машина приема, че има достъп до хранилище от надеждни класове, което може да се изпълнява от виртуалната машина без проверка.

Първоначалният зареждащ клас реализира изпълнението по подразбиране на loadClass () . По този начин този код разбира, че името на класа java.lang.Object се съхранява във файл с префикс java / lang / Object.class някъде в пътя на класа. Този код също така реализира както търсене по пътя на класа, така и разглеждане на zip файлове за класове. Наистина страхотното в начина, по който това е проектирано, е, че Java може да промени модела си за съхранение на класове, просто като промени набора от функции, които реализират зареждащия клас.

Разровявайки се в червата на виртуалната машина на Java, ще откриете, че първоначалният зареждащ клас е реализиран предимно във функциите FindClassFromClass и ResolveClass .

И така, кога се зареждат класовете? Има точно два случая: когато се изпълнява новият байт код (например FooClass f = нов FooClass () ;) и когато байт кодовете правят статична препратка към клас (например System. Out ).

Товарач с не първичен клас

- И какво? може да попитате.

Виртуалната машина Java има куки в себе си, за да позволи на потребителя дефиниран зареждащ клас да се използва вместо първоначалния. Освен това, тъй като зареждащият потребителски клас получава първа пукнатина в името на класа, потребителят е в състояние да внедри произволен брой интересни хранилища на класове, не на последно място от които са HTTP сървърите - които на първо място извадиха Java от земята.

Има обаче цена, тъй като зареждачът на класове е толкова мощен (например, той може да замени java.lang.Object със собствена версия), Java класове като аплети нямат право да създават свои собствени зареждачи. (Това между другото се налага от зареждащия клас.) Тази колона няма да е полезна, ако се опитвате да правите тези неща с аплет, само с приложение, изпълнявано от хранилището на надеждни класове (като локални файлове).

Зареждачът на потребителски клас получава шанс да зареди клас преди първоначалния товарач на клас. Поради това той може да зареди данните за изпълнение на класа от някакъв алтернативен източник, което е начинът, по който AppletClassLoader може да зарежда класове, използвайки HTTP протокола.

Изграждане на SimpleClassLoader

Зареждащият клас започва като е подклас на java.lang.ClassLoader . Единственият абстрактен метод, който трябва да бъде внедрен, е loadClass () . Потокът на loadClass () е както следва:

  • Проверете името на класа.
  • Проверете дали заявеният клас вече е зареден.
  • Проверете дали класът е "системен" клас.
  • Опит за извличане на класа от хранилището на този клас.
  • Дефинирайте класа за виртуалната машина.
  • Разрешете класа.
  • Върнете класа на повикващия.

SimpleClassLoader се появява, както следва, с описания за това, което прави, разпръснати в кода.

публичен синхронизиран клас loadClass (String className, boolean resolIt) хвърля ClassNotFoundException {Резултат от клас; байт classData []; System.out.println (">>>>>> Клас на зареждане:" + className); / * Проверете нашия локален кеш от класове * / result = (Class) classes.get (className); if (result! = null) {System.out.println (">>>>>> връщане на кеширан резултат."); връщане на резултат; }

Кодът по-горе е първият раздел на метода loadClass . Както можете да видите, е необходимо име на клас и търси в локална хеш таблица, която нашият зареждащ клас поддържа на класове, които вече е върнал. Важно е да запазите тази хеш таблица наоколо, тъй като трябва да връщате една и съща препратка към обект на клас за едно и също име на класа всеки път, когато бъдете помолени за това. В противен случай системата ще повярва, че има два различни класа с едно и също име и ще изхвърли ClassCastException всеки път, когато присвоите препратка към обект между тях. Също така е важно да запазите кеш, защото loadClass () метод се извиква рекурсивно, когато клас се разрешава и ще трябва да върнете кеширания резултат, вместо да го преследвате за друго копие.

/* Check with the primordial class loader */ try { result = super.findSystemClass(className); System.out.println(" >>>>>> returning system class (in CLASSPATH)."); return result; } catch (ClassNotFoundException e) { System.out.println(" >>>>>> Not a system class."); } 

As you can see in the code above, the next step is to check if the primordial class loader can resolve this class name. This check is essential to both the sanity and security of the system. For example, if you return your own instance of java.lang.Object to the caller, then this object will share no common superclass with any other object! The security of the system can be compromised if your class loader returned its own value of java.lang.SecurityManager, which did not have the same checks as the real one did.

 /* Try to load it from our repository */ 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); 

If the class implementation was loaded, the penultimate step is to call the defineClass() method from java.lang.ClassLoader, which can be considered the first step of class verification. This method is implemented in the Java virtual machine and is responsible for verifying that the class bytes are a legal Java class file. Internally, the defineClass method fills out a data structure that the JVM uses to hold classes. If the class data is malformed, this call will cause a ClassFormatError to be thrown.

 if (resolveIt) { resolveClass(result); } 

The last class loader-specific requirement is to call resolveClass() if the boolean parameter resolveIt was true. This method does two things: First, it causes any classes that are referenced by this class explicitly to be loaded and a prototype object for this class to be created; then, it invokes the verifier to do dynamic verification of the legitimacy of the bytecodes in this class. If verification fails, this method call will throw a LinkageError, the most common of which is a 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.

Най-добрият пример за първата техника е уеб браузър. Класът, дефиниран от Java, който се изпълнява от всички аплети, е java.applet.Applet . Когато клас се зарежда от AppletClassLoader , създаденият екземпляр на обект се прехвърля към екземпляр на Applet . Ако този актьор успее, се извиква методът init () . В моя пример използвам втората техника, интерфейс.

Игра с примера

За да завърша примера, създадох още няколко

.java

файлове. Това са:

публичен интерфейс LocalModule {/ * Стартиране на модула * / void start (опция String); }