Рубрики
Без рубрики

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

Взгляните на закулисье загрузки классов Java во время выполнения в JVM.

Автор оригинала: baeldung.

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

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

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

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

Дальнейшее чтение:

Понимание утечек памяти в Java

ClassNotFoundException против NoClassDefFoundError

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:[email protected]
Class loader of Logging:[email protected]
Class loader of ArrayList:null

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

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

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

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

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

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

2.1. Загрузчик классов начальной загрузки

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

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

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

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

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

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

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

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

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

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

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

То ява,ланг.Загрузчик классов.loadClass() метод отвечает за загрузку определения класса во время выполнения . Он пытается загрузить класс на основе полного имени.

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

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

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

Давайте рассмотрим пример вывода, когда возникает исключение 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)

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

Когда он все еще не находит класс, он создает исключение ClassNotFoundException.

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

3.1. Модель делегирования полномочий

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

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

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

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

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

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

3.3. Видимость

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Нам нужно расширить Загрузчик классов класс и переопределить 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.Загрузчик классов

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

5.1. Метод loadClass()

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

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

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

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

Мы можем попытаться понять внутреннюю работу метода loadClass() из исходного кода java.lang.Загрузчик классов:

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 необходимо динамически загружать классы или ресурсы, предоставляемые разработчиками приложений, мы можем столкнуться с проблемой.

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

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

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

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

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

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

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

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

Примеры кода, как всегда, можно найти на GitHub .