Перейти к основному содержимому

Загрузчики классов в Java

· 11 мин. чтения

1. Введение в загрузчики классов

Загрузчики классов отвечают за динамическую загрузку классов Java в JVM (виртуальную машину Java) во время выполнения. Они также являются частью JRE (Java Runtime Environment). Следовательно, JVM не нужно знать о базовых файлах или файловых системах для запуска программ Java благодаря загрузчикам классов.

Кроме того, эти классы Java загружаются в память не сразу, а тогда, когда они требуются приложению. Здесь на помощь приходят загрузчики классов. Они отвечают за загрузку классов в память.

В этом руководстве мы поговорим о различных типах встроенных загрузчиков классов и о том, как они работают. Затем мы представим нашу собственную реализацию.

2. Типы встроенных загрузчиков классов

Давайте начнем с изучения того, как мы можем загружать разные классы, используя различные загрузчики классов:

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());
}

При выполнении вышеуказанный метод печатает:

Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null

Как мы видим, здесь есть три разных загрузчика классов: приложение, расширение и загрузчик (отображается как null ).

Загрузчик класса приложения загружает класс, в котором содержится пример метода. Загрузчик класса приложения или системы загружает наши собственные файлы в путь к классам.

Затем загрузчик класса расширения загружает класс Logging . Загрузчики классов расширения загружают классы, которые являются расширением стандартных базовых классов Java.

Наконец, загрузчик классов начальной загрузки загружает класс ArrayList . Начальный загрузчик или первичный загрузчик классов является родителем всех остальных.

Однако мы видим, что для ArrayList в выходных данных отображается null . Это связано с тем, что загрузчик классов начальной загрузки написан на собственном коде, а не на Java, поэтому он не отображается как класс Java. В результате поведение загрузчика классов начальной загрузки будет различаться для разных JVM.

Теперь давайте обсудим каждый из этих загрузчиков классов более подробно.

2.1. Загрузчик классов Bootstrap

Классы Java загружаются экземпляром java.lang.ClassLoader . Однако загрузчики классов сами по себе являются классами. Итак, вопрос в том, кто загружает сам java.lang.ClassLoader ?

Именно здесь вступает в игру начальная загрузка или первоначальный загрузчик классов.

Он в основном отвечает за загрузку внутренних классов JDK, обычно rt.jar и других основных библиотек, расположенных в каталоге $JAVA_HOME/jre/lib . Кроме того, загрузчик классов Bootstrap служит родителем всех остальных экземпляров ClassLoader .

Этот загрузчик классов начальной загрузки является частью основной JVM и написан на машинном коде, как указано в приведенном выше примере. На разных платформах могут быть разные реализации этого конкретного загрузчика классов.

2.2. Загрузчик класса расширения

Загрузчик классов расширений является потомком загрузчика классов начальной загрузки и обеспечивает загрузку расширений стандартных основных классов Java , чтобы они были доступны для всех приложений, работающих на платформе.

Загрузчик класса расширения загружается из каталога расширений JDK, обычно это каталог $JAVA_HOME/lib/ext или любой другой каталог, указанный в системном свойстве java.ext.dirs .

2.3. Загрузчик системных классов

С другой стороны, загрузчик классов системы или приложения заботится о загрузке всех классов уровня приложения в JVM. Он загружает файлы, найденные в переменной среды classpath, -classpath или параметре командной строки -cp . Это также дочерний элемент загрузчика классов расширений.

3. Как работают загрузчики классов?

Загрузчики классов являются частью среды выполнения Java. Когда JVM запрашивает класс, загрузчик классов пытается найти класс и загрузить определение класса в среду выполнения, используя полное имя класса.

Метод java.lang.ClassLoader.loadClass() отвечает за загрузку определения класса в среду выполнения . Он пытается загрузить класс на основе полного имени.

Если класс еще не загружен, он делегирует запрос загрузчику родительского класса. Этот процесс происходит рекурсивно.

В конце концов, если загрузчик родительского класса не найдет класс, то дочерний класс вызовет метод java.net.URLClassLoader.findClass() для поиска классов в самой файловой системе.

Если последний загрузчик дочернего класса также не может загрузить класс, он генерирует исключение java.lang.NoClassDefFoundError или java.lang.ClassNotFoundException.

Давайте посмотрим на пример вывода, когда выдается ClassNotFoundException :

java.lang.ClassNotFoundException: com.foreach.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)

Если мы пройдем последовательность событий сразу после вызова java.lang.Class.forName() , мы увидим, что сначала он пытается загрузить класс через загрузчик родительского класса, а затем java.net.URLClassLoader.findClass() для ищите сам класс.

Когда он по-прежнему не находит класс, он генерирует исключение ClassNotFoundException.

Теперь давайте рассмотрим три важные особенности загрузчиков классов.

3.1. Модель делегирования

Загрузчики классов следуют модели делегирования, где при запросе на поиск класса или ресурса экземпляр ClassLoader делегирует поиск класса или ресурса родительскому загрузчику классов .

Допустим, у нас есть запрос на загрузку класса приложения в JVM. Загрузчик системного класса сначала делегирует загрузку этого класса своему родительскому загрузчику классов расширения, который, в свою очередь, делегирует его загрузчику классов начальной загрузки.

Только если начальная загрузка, а затем загрузчик классов расширения не могут загрузить класс, системный загрузчик классов пытается загрузить сам класс.

3.2. Уникальные классы

Благодаря модели делегирования легко обеспечить уникальные классы, поскольку мы всегда пытаемся делегировать вверх .

Если загрузчик родительского класса не может найти класс, только тогда текущий экземпляр попытается сделать это самостоятельно.

3.3. Видимость

Кроме того, дочерние загрузчики классов видны классам, загруженным их родительскими загрузчиками классов .

Например, классы, загруженные системным загрузчиком классов, имеют видимость классов, загруженных расширением и загрузчиками классов начальной загрузки, но не наоборот.

Чтобы проиллюстрировать это, если класс A загружается загрузчиком классов приложения, а класс B загружается загрузчиком классов расширений, то классы A и B видны для других классов, загруженных загрузчиком классов приложения.

Однако класс B является единственным классом, видимым для других классов, загружаемых загрузчиком классов расширения.

4. Пользовательский загрузчик классов

Встроенного загрузчика классов достаточно для большинства случаев, когда файлы уже находятся в файловой системе.

Однако в сценариях, когда нам нужно загружать классы с локального жесткого диска или из сети, нам может потребоваться использовать пользовательские загрузчики классов.

В этом разделе мы рассмотрим некоторые другие варианты использования пользовательских загрузчиков классов и продемонстрируем, как их создать.

4.1. Варианты использования пользовательских загрузчиков классов

Пользовательские загрузчики классов полезны не только для загрузки класса во время выполнения. Несколько вариантов использования могут включать:

  1. Помощь в изменении существующего байт-кода, например, ткацких агентов.
  2. Создание классов, динамически подходящих для нужд пользователя, например, в JDBC переключение между различными реализациями драйверов осуществляется посредством динамической загрузки классов.
  3. Реализация механизма управления версиями классов при загрузке разных байт-кодов для классов с одинаковыми именами и пакетами. Это можно сделать либо с помощью загрузчика классов URL (загрузка jar-файлов через URL-адреса), либо с помощью пользовательских загрузчиков классов.

Ниже приведены более конкретные примеры, где могут пригодиться пользовательские загрузчики классов.

Браузеры, например, используют пользовательский загрузчик классов для загрузки исполняемого содержимого с веб-сайта. Браузер может загружать апплеты с разных веб-страниц, используя отдельные загрузчики классов. Средство просмотра апплетов, которое используется для запуска апплетов, содержит ClassLoader , который обращается к веб-сайту на удаленном сервере вместо поиска в локальной файловой системе.

Затем он загружает необработанные файлы байт-кода через HTTP и превращает их в классы внутри JVM. Даже если эти апплеты имеют одинаковое имя, они считаются разными компонентами, если загружаются разными загрузчиками классов .

Теперь, когда мы понимаем, почему пользовательские загрузчики классов важны, давайте реализуем подкласс ClassLoader , чтобы расширить и суммировать функциональные возможности того, как JVM загружает классы.

4.2. Создание нашего пользовательского загрузчика классов

Для иллюстрации предположим, что нам нужно загрузить классы из файла с помощью пользовательского загрузчика классов.

Нам нужно расширить класс ClassLoader и переопределить метод findClass() :

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;
}
}

В приведенном выше примере мы определили собственный загрузчик классов, который расширяет загрузчик классов по умолчанию и загружает массив байтов из указанного файла.

5. Понимание java.lang.ClassLoader

Давайте обсудим несколько основных методов класса java.lang.ClassLoader , чтобы получить более четкое представление о том, как он работает.

5.1. Метод loadClass( )

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

Этот метод отвечает за загрузку класса с заданным параметром имени. Параметр имени относится к полному имени класса.

Виртуальная машина Java вызывает метод loadClass() для разрешения ссылок на классы, задавая для разрешения значение true . Однако не всегда необходимо разрешать класс. Если нам нужно только определить, существует ли класс или нет, то параметр разрешения устанавливается равным false .

Этот метод служит точкой входа для загрузчика классов.

Мы можем попытаться понять внутреннюю работу метода loadClass() из исходного кода 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;
}
}

Реализация метода по умолчанию ищет классы в следующем порядке:

  1. Вызывает метод findLoadedClass(String) , чтобы узнать, загружен ли уже класс.
  2. Вызывает метод loadClass(String) в загрузчике родительского класса.
  3. Вызовите метод findClass (String) , чтобы найти класс.

5.2. Метод defineClass( )

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

Этот метод отвечает за преобразование массива байтов в экземпляр класса. Прежде чем использовать класс, нам нужно разрешить его.

Если данные не содержат допустимого класса, выдается ошибка ClassFormatError.

Кроме того, мы не можем переопределить этот метод, так как он помечен как окончательный.

5.3. Метод findClass( )

protected Class<?> findClass(
String name) throws ClassNotFoundException

Этот метод находит класс с полным именем в качестве параметра. Нам нужно переопределить этот метод в пользовательских реализациях загрузчика классов, которые следуют модели делегирования для загрузки классов.

Кроме того, loadClass() вызывает этот метод, если загрузчик родительского класса не может найти запрошенный класс.

Реализация по умолчанию генерирует исключение ClassNotFoundException , если ни один из родителей загрузчика класса не находит класс.

5.4. Метод getParent( )

public final ClassLoader getParent()

Этот метод возвращает загрузчик родительского класса для делегирования.

В некоторых реализациях, таких как показанная ранее в разделе 2, для представления загрузчика классов начальной загрузки используется значение null .

5.5. Метод getResource( )

public URL getResource(String name)

Этот метод пытается найти ресурс с заданным именем.

Сначала он делегирует ресурс загрузчику родительского класса. Если родитель имеет значение null , ищется путь к загрузчику классов, встроенному в виртуальную машину.

Если это не удается, метод вызовет findResource(String) для поиска ресурса. Имя ресурса, указанное в качестве входных данных, может быть относительным или абсолютным по отношению к пути к классам.

Он возвращает объект URL для чтения ресурса или значение null, если ресурс не может быть найден или вызывающий объект не имеет достаточных привилегий для возврата ресурса.

Важно отметить, что Java загружает ресурсы из пути к классам.

Наконец, загрузка ресурсов в Java считается независимой от местоположения, поскольку не имеет значения, где выполняется код, пока среда настроена на поиск ресурсов.

6. Контекстные загрузчики классов

В общем, загрузчики классов контекста предоставляют альтернативный метод схеме делегирования загрузки классов, введенной в J2SE.

Как мы узнали ранее, загрузчики классов в JVM следуют иерархической модели, так что каждый загрузчик классов имеет одного родителя, за исключением загрузчика классов начальной загрузки.

Однако иногда, когда базовым классам JVM необходимо динамически загружать классы или ресурсы, предоставленные разработчиками приложений, мы можем столкнуться с проблемой.

Например, в JNDI основная функциональность реализуется классами начальной загрузки в rt.jar. Но эти классы JNDI могут загружать поставщиков JNDI, реализованных независимыми поставщиками (развернутыми в пути к классам приложения). Этот сценарий требует, чтобы загрузчик классов начальной загрузки (загрузчик родительских классов) загружал класс, видимый для загрузчика приложений (загрузчик дочерних классов).

Делегирование J2SE здесь не работает, и чтобы обойти эту проблему, нам нужно найти альтернативные способы загрузки классов. Этого можно добиться с помощью загрузчиков контекста потока.

Класс java.lang.Thread имеет метод getContextClassLoader(), который возвращает ContextClassLoader для конкретного потока . ContextClassLoader предоставляется создателем потока при загрузке ресурсов и классов.

Если значение не установлено, то по умолчанию используется контекст загрузчика классов родительского потока.

7. Заключение

Загрузчики классов необходимы для выполнения программы Java. В этой статье мы познакомили их с ними.

Мы обсудили различные типы загрузчиков классов, а именно загрузчики классов Bootstrap, Extensions и System. Bootstrap служит родителем для всех них и отвечает за загрузку внутренних классов JDK. С другой стороны, расширения и система загружают классы из каталога расширений Java и пути к классам соответственно.

Мы также узнали, как работают загрузчики классов, и изучили некоторые функции, такие как делегирование, видимость и уникальность. Затем мы кратко объяснили, как создать собственный загрузчик классов. Наконец, мы познакомили вас с загрузчиками класса Context.

Как всегда, исходный код этих примеров можно найти на GitHub .