Moduły ładujące klasy w Javie

1. Wprowadzenie do programów ładujących klas

Programy ładujące klasy są odpowiedzialne za dynamiczne ładowanie klas Java w czasie wykonywania do maszyny JVM (wirtualnej maszyny języka Java). Są również częścią środowiska JRE (Java Runtime Environment). W związku z tym JVM nie musi wiedzieć o plikach lub systemach plików bazowych, aby uruchamiać programy Java dzięki programom ładującym klasy.

Ponadto te klasy Java nie są ładowane do pamięci jednocześnie, ale gdy wymagają tego aplikacja. W tym miejscu pojawiają się ładowarki klas. Odpowiadają za ładowanie klas do pamięci.

W tym samouczku omówimy różne typy wbudowanych programów ładujących klasy, sposób ich działania oraz wprowadzenie do naszej własnej niestandardowej implementacji.

2. Typy wbudowanych programów ładujących klasy

Zacznijmy od nauczenia się, jak różne klasy są ładowane za pomocą różnych programów ładujących klasy, korzystając z prostego przykładu:

public void printClassLoaders() throws ClassNotFoundException { System.out.println("Classloader of this class:" + PrintClassLoader.class.getClassLoader()); System.out.println("Classloader of Logging:" + Logging.class.getClassLoader()); System.out.println("Classloader of ArrayList:" + ArrayList.class.getClassLoader()); }

Po wykonaniu powyższej metody drukuje:

Class loader of this class:[email protected] Class loader of Logging:[email protected] Class loader of ArrayList:null

Jak widać, są tutaj trzy różne programy ładujące klasy; aplikacja, rozszerzenie i bootstrap (wyświetlane jako null ).

Program ładujący klasy aplikacji ładuje klasę, w której znajduje się przykładowa metoda. Program ładujący klasę aplikacji lub systemu ładuje nasze własne pliki w ścieżce klas.

Następnie rozszerzenie ładuje klasę Logging . Moduły ładujące klasy rozszerzeń ładują klasy, które są rozszerzeniem standardowych podstawowych klas języka Java.

Na koniec bootstrap ładuje klasę ArrayList . Program ładujący lub pierwotny program ładujący klasy jest rodzicem wszystkich pozostałych.

Jednak widzimy, że ostatnie wyjście, dla ArrayList , wyświetla wartość null w danych wyjściowych. Dzieje się tak, ponieważ program ładujący klasy bootstrap jest napisany w kodzie natywnym, a nie w Javie - więc nie jest wyświetlany jako klasa Java. Z tego powodu zachowanie programu ładującego klasy ładowania początkowego będzie się różnić w różnych maszynach JVM.

Omówmy teraz bardziej szczegółowo każdy z tych programów ładujących klasy.

2.1. Moduł ładujący klasy Bootstrap

Klasy Java są ładowane przez instancję java.lang.ClassLoader . Jednak programy ładujące klasy same w sobie są klasami. W związku z tym pojawia się pytanie, kto ładuje sam java.lang.ClassLoader ?

W tym miejscu pojawia się bootstrap lub pierwotny program ładujący klasy.

Jest głównie odpowiedzialny za ładowanie wewnętrznych klas JDK, zazwyczaj rt.jar i innych podstawowych bibliotek znajdujących się w katalogu $ JAVA_HOME / jre / lib . Ponadto moduł ładujący klasy Bootstrap służy jako element nadrzędny wszystkich innych instancji ClassLoader .

Ten program ładujący klasę ładującą jest częścią podstawowej maszyny JVM i jest napisany w kodzie natywnym, jak wskazano w powyższym przykładzie. Różne platformy mogą mieć różne implementacje tego konkretnego programu ładującego klasy.

2.2. Moduł ładujący klasy rozszerzeń

Moduł ładujący klasy rozszerzeń jest elementem potomnym programu ładującego klasę ładującą i dba o ładowanie rozszerzeń standardowych podstawowych klas Java, tak aby był dostępny dla wszystkich aplikacji działających na platformie.

Program ładujący klasy rozszerzeń ładuje się z katalogu rozszerzeń JDK, zwykle z katalogu $ JAVA_HOME / lib / ext lub dowolnego innego katalogu wymienionego we właściwości systemowej java.ext.dirs .

2.3. Program ładujący klasy systemu

Z drugiej strony moduł ładujący klasy systemu lub aplikacji zajmuje się ładowaniem wszystkich klas poziomu aplikacji do maszyny JVM. Ładuje pliki znalezione w zmiennej środowiskowej classpath, opcji -classpath lub -cp wiersza poleceń . Jest to również element podrzędny modułu ładującego klasy rozszerzeń.

3. Jak działają programy ładujące klasy?

Programy ładujące klasy są częścią środowiska Java Runtime Environment. Gdy maszyna JVM żąda klasy, program ładujący klasy próbuje zlokalizować klasę i załadować definicję klasy do środowiska wykonawczego przy użyciu w pełni kwalifikowanej nazwy klasy.

Metoda java.lang.ClassLoader.loadClass () odpowiada za ładowanie definicji klasy do środowiska wykonawczego . Próbuje załadować klasę na podstawie w pełni kwalifikowanej nazwy.

Jeśli klasa nie jest jeszcze załadowana, deleguje żądanie do modułu ładującego klasy nadrzędnej. Ten proces odbywa się rekurencyjnie.

Ostatecznie, jeśli program ładujący klasę nadrzędną nie znajdzie klasy, klasa potomna wywoła metodę java.net.URLClassLoader.findClass () w celu wyszukania klas w samym systemie plików.

Jeśli ostatni program ładujący klasy podrzędnej również nie może załadować klasy, zgłasza java.lang.NoClassDefFoundError lub java.lang.ClassNotFoundException.

Spójrzmy na przykład danych wyjściowych, gdy zostanie zgłoszony wyjątek ClassNotFoundException.

java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348)

If we go through the sequence of events right from calling java.lang.Class.forName(), we can understand that it first tries to load the class through parent class loader and then java.net.URLClassLoader.findClass() to look for the class itself.

When it still doesn't find the class, it throws a ClassNotFoundException.

There are three important features of class loaders.

3.1. Delegation Model

Class loaders follow the delegation model where on request to find a class or resource, a ClassLoader instance will delegate the search of the class or resource to the parent class loader.

Let's say we have a request to load an application class into the JVM. The system class loader first delegates the loading of that class to its parent extension class loader which in turn delegates it to the bootstrap class loader.

Only if the bootstrap and then the extension class loader is unsuccessful in loading the class, the system class loader tries to load the class itself.

3.2. Unique Classes

As a consequence of the delegation model, it's easy to ensure unique classes as we always try to delegate upwards.

If the parent class loader isn't able to find the class, only then the current instance would attempt to do so itself.

3.3. Visibility

In addition, children class loaders are visible to classes loaded by its parent class loaders.

For instance, classes loaded by the system class loader have visibility into classes loaded by the extension and Bootstrap class loaders but not vice-versa.

To illustrate this, if Class A is loaded by an application class loader and class B is loaded by the extensions class loader, then both A and B classes are visible as far as other classes loaded by Application class loader are concerned.

Class B, nonetheless, is the only class visible as far as other classes loaded by the extension class loader are concerned.

4. Custom ClassLoader

The built-in class loader would suffice in most of the cases where the files are already in the file system.

However, in scenarios where we need to load classes out of the local hard drive or a network, we may need to make use of custom class loaders.

In this section, we'll cover some other uses cases for custom class loaders and we'll demonstrate how to create one.

4.1. Custom Class Loaders Use-Cases

Custom class loaders are helpful for more than just loading the class during runtime, a few use cases might include:

  1. Helping in modifying the existing bytecode, e.g. weaving agents
  2. Creating classes dynamically suited to the user's needs. e.g in JDBC, switching between different driver implementations is done through dynamic class loading.
  3. Implementing a class versioning mechanism while loading different bytecodes for classes with same names and packages. This can be done either through URL class loader (load jars via URLs) or custom class loaders.

There are more concrete examples where custom class loaders might come in handy.

Browsers, for instance, use a custom class loader to load executable content from a website. A browser can load applets from different web pages using separate class loaders. The applet viewer which is used to run applets contains a ClassLoader that accesses a website on a remote server instead of looking in the local file system.

And then loads the raw bytecode files via HTTP, and turns them into classes inside the JVM. Even if these applets have the same name, they are considered as different components if loaded by different class loaders.

Now that we understand why custom class loaders are relevant, let's implement a subclass of ClassLoader to extend and summarise the functionality of how the JVM loads classes.

4.2. Creating Our Custom Class Loader

For illustration purposes, let's say we need to load classes from a file using a custom class loader.

We need to extend the ClassLoader class and override the findClass() method:

public class CustomClassLoader extends ClassLoader { @Override public Class findClass(String name) throws ClassNotFoundException { byte[] b = loadClassFromFile(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassFromFile(String fileName) { InputStream inputStream = getClass().getClassLoader().getResourceAsStream( fileName.replace('.', File.separatorChar) + ".class"); byte[] buffer; ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); int nextValue = 0; try { while ( (nextValue = inputStream.read()) != -1 ) { byteStream.write(nextValue); } } catch (IOException e) { e.printStackTrace(); } buffer = byteStream.toByteArray(); return buffer; } }

In the above example, we defined a custom class loader that extends the default class loader and loads a byte array from the specified file.

5. Understanding java.lang.ClassLoader

Let's discuss a few essential methods from the java.lang.ClassLoader class to get a clearer picture of how it works.

5.1. The loadClass() Method

public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {

This method is responsible for loading the class given a name parameter. The name parameter refers to the fully qualified class name.

The Java Virtual Machine invokes loadClass() method to resolve class references setting resolve to true. However, it isn't always necessary to resolve a class. If we only need to determine if the class exists or not, then resolve parameter is set to false.

This method serves as an entry point for the class loader.

We can try to understand the internal working of the loadClass() method from the source code of java.lang.ClassLoader:

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }

The default implementation of the method searches for classes in the following order:

  1. Invokes the findLoadedClass(String) method to see if the class is already loaded.
  2. Invokes the loadClass(String) method on the parent class loader.
  3. Invoke the findClass(String) method to find the class.

5.2. The defineClass() Method

protected final Class defineClass( String name, byte[] b, int off, int len) throws ClassFormatError

This method is responsible for the conversion of an array of bytes into an instance of a class. And before we use the class, we need to resolve it.

In case data didn't contain a valid class, it throws a ClassFormatError.

Also, we can't override this method since it's marked as final.

5.3. The findClass() Method

protected Class findClass( String name) throws ClassNotFoundException

This method finds the class with the fully qualified name as a parameter. We need to override this method in custom class loader implementations that follow the delegation model for loading classes.

Also, loadClass() invokes this method if the parent class loader couldn't find the requested class.

The default implementation throws a ClassNotFoundException if no parent of the class loader finds the class.

5.4. The getParent() Method

public final ClassLoader getParent()

This method returns the parent class loader for delegation.

Some implementations like the one seen before in Section 2. use null to represent the bootstrap class loader.

5.5. The getResource() Method

public URL getResource(String name)

This method tries to find a resource with the given name.

It will first delegate to the parent class loader for the resource. If the parent is null, the path of the class loader built into the virtual machine is searched.

If that fails, then the method will invoke findResource(String) to find the resource. The resource name specified as an input can be relative or absolute to the classpath.

It returns an URL object for reading the resource, or null if the resource could not be found or if the invoker doesn't have adequate privileges to return the resource.

It's important to note that Java loads resources from the classpath.

Finally, resource loading in Java is considered location-independent as it doesn't matter where the code is running as long as the environment is set to find the resources.

6. Context Classloaders

In general, context class loaders provide an alternative method to the class-loading delegation scheme introduced in J2SE.

Like we've learned before, classloaders in a JVM follow a hierarchical model such that every class loader has a single parent with the exception of the bootstrap class loader.

However, sometimes when JVM core classes need to dynamically load classes or resources provided by application developers, we might encounter a problem.

For example, in JNDI the core functionality is implemented by bootstrap classes in rt.jar. But these JNDI classes may load JNDI providers implemented by independent vendors (deployed in the application classpath). This scenario calls for the bootstrap class loader (parent class loader) to load a class visible to application loader (child class loader).

J2SE delegation doesn't work here and to get around this problem, we need to find alternative ways of class loading. And it can be achieved using thread context loaders.

The java.lang.Thread class has a method getContextClassLoader() that returns the ContextClassLoader for the particular thread. The ContextClassLoader is provided by the creator of the thread when loading resources and classes.

If the value isn't set, then it defaults to the class loader context of the parent thread.

7. Conclusion

Class loaders are essential to execute a Java program. We've provided a good introduction as part of this article.

Rozmawialiśmy o różnych typach programów ładujących klasy, a mianowicie - ładujących klasach Bootstrap, Extensions i System. Bootstrap pełni rolę rodzica dla nich wszystkich i jest odpowiedzialny za ładowanie wewnętrznych klas JDK. Z drugiej strony, rozszerzenia i system ładują klasy odpowiednio z katalogu rozszerzeń Java i ścieżki klas.

Następnie rozmawialiśmy o tym, jak działają programy ładujące klasy i omówiliśmy niektóre funkcje, takie jak delegowanie, widoczność i unikalność, a następnie krótkie wyjaśnienie, jak utworzyć niestandardowy. Na koniec udostępniliśmy wprowadzenie do programów ładujących klasy Context.

Przykłady kodu, jak zawsze, można znaleźć na GitHub.