Глубокое погружение в Java: рефлексия и загрузчик классов. Часть 2

Первая часть статьи.

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

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

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

JVM

В Java используется гибридный подход к выполнению программного кода, сочетающий компиляцию и интерпретацию. Сначала класс Java определяется программистом в формате файла .java. Затем компилятор Java преобразует его в байт-код, генерируя файл .class. Байт-код  —  это промежуточный язык, который понимает JVM.

Java-программы выполняются с помощью виртуальной машины Java (JVM). Идея виртуальной машины Java заключается в том, что она действует как абстрактный виртуальный компьютер, который создает изолированную среду для выполнения Java-программ. Работающая JVM практически не знает о своем хост-компьютере и определяет строгий протокол загрузки программных файлов из файловой системы компьютера для выполнения.

JVM включает в себя три отдельные области:

  • подсистему загрузчика классов (ClassLoader Subsystem);
  • область памяти/данных времени выполнения (Runtime Memory/Data Area);
  • механизм выполнения (Execution Engine).

Подсистема загрузчика классов  —  это единственный компонент JVM, который работает с файловой системой хост-устройства. Он отвечает за поиск файлов классов и их загрузку в JVM. При запросе на загрузку класса система загрузчика классов считывает байт-код его файла класса в JVM, затем конструирует эквивалентный объект Class и сохраняет байт-код его метода в куче (напомним, что классы существуют внутри JVM как объекты Class). Эти шаги показаны на следующей схеме:

Изображение автора

Процесс загрузки классов в JVM является динамическим, т.е. классы загружаются не заранее, а по требованию. Это называется ленивой загрузкой. При запуске программы JVM загружает класс, содержащий метод main(), и все классы, на которые он ссылается. В дальнейшем при появлении нового типа подсистема загрузчика классов запрашивает его загрузку. Пользователи могут определять собственные загрузчики классов и интегрировать их с системой загрузки классов для выполнения пользовательской загрузки классов.

Подсистема загрузчика классов

Подсистема загрузчика классов состоит из нескольких совместно работающих компонентов, каждый из которых способен загружать определенные классы. Не стоит удивляться тому, что загрузчики классов моделируются как классы в JVM. Отдельный загрузчик классов является объектом специального внутреннего класса Java (Java  —  это Java). В пакете java.lang определен абстрактный тип ClassLoader, который является классом, отвечающим за загрузку других классов в JVM.

public abstract class ClassLoader extends Object

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

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

Загрузчики классов построены в виде иерархии с отношениями “родитель-ребенок”, то есть объект загрузчика классов является дочерним для другого загрузчика классов и, возможно, родительским для некоторых других загрузчиков классов. Существует три основных загрузчика классов, используемых во время выполнения программы.

  • Bootstrap ClassLoader, также известный как Primordial ClassLoader и Null ClassLoader. Он запускает среду выполнения. Это корневой загрузчик классов и единственный, который не является классом Java, а представляет собой платформозависимый машинный код, загружающий основные классы базовой среды выполнения (Java Runtime Environment, JRE), включая классы пакетов java.util и java.lang из пути jdk/jre/lib/. Пакет java.lang содержит такие основные классы, как Object, String и класс ClassLoader, который обеспечивает инициализацию всех последующих загрузчиков классов.
  • Extension ClassLoader является дочерним по отношению к Bootstrap ClassLoader. Он загружает основные расширения классов Java, расположенные по пути jdk/jre/lib/ext/. Расширения Java  —  это дополнительные пакеты классов, которые своими функциями дополняют платформу Java.
  • Application ClassLoader, также известный как System ClassLoader, является дочерним по отношению к Extension ClassLoader. Он загружает специфические для приложения классы из каталогов и хранилищ, указанных переменной среды CLASSPATH или системным свойством java.class.path. В этом пути находятся классы, которые программист определяет для своего конкретного приложения.

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

Определение того, какой загрузчик классов загружает тот или иной класс, осуществляется по принципу иерархии делегирования. Согласно этому принципу, когда загрузчик классов получает запрос на загрузку класса, он сначала пытается делегировать этот запрос своему родителю. Если родительскому загрузчику не удается загрузить класс, то он пытается загрузить его самостоятельно, отыскивая файл .class в определенных путях. На приведенной ниже диаграмме показан обобщенный обзор шагов, выполняемых подсистемой загрузчика классов при получении запроса на загрузку класса:

Изображение автора

Если запрашиваемый класс является специфичным для приложения классом, определенным программистом, то он будет существовать в системной переменной classpath. Первоначально запрос получает Application ClassLoader. Он делегирует его Extension ClassLoader, который, в свою очередь, делегирует его Bootstrap ClassLoader. Bootstrap ClassLoader не может найти класс в своих путях, поэтому ответственность делегируется обратно Extension ClassLoader, который также не справляется. Затем ответственность передается Application ClassLoader, который должен найти класс, если он существует.

Класс ClassLoader

Абстрактный класс ClassLoader расширяется для определения конкретных загрузчиков классов в системе, таких как Extension ClassLoader и Application ClassLoader. ClassLoader определяет 4 основных метода, используемых при загрузке классов:

  • Class defineClass(byte[] b, int off, int len). Этот метод является основным при загрузке классов и объявляется как final. Сначала файл класса считывается в байтовый массив. Метод defineClass принимает этот байтовый массив в качестве входных данных и возвращает эквивалентный объект Class.
  • Class findLoadedClass(String name). Этот метод проверяет, не загружен ли уже класс, и возвращает его объект Class или null, если класс с таким именем не найден.
  • Class findClass(String name). Этот метод пытается найти файл класса по определенным путям и загрузить его данные (байтовый массив). Затем с помощью defineClass создается эквивалентный объект Class.
  • Class loadClass(String name, boolean resolve). Любая загрузка класса начинается с вызова этого метода. Он использует все вышеперечисленные методы и реализует модель делегирования. Каркас этого метода показан ниже:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// проверка того, был ли класс уже загружен
Class<?> c = findLoadedClass(name);

if (c == null) { // если класс не был загружен ранее
// ...
try {
if (parent != null) {
// запрос у родителя на загрузку класса
c = parent.loadClass(name, false);
} else {
// если непосредственного родителя нет в наличии, запрос на загрузку отправляется BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// выбрасывается ClassNotFoundException,
// если у родителя не получается загрузить класс
}

if (c == null) { // родитель не смог загрузить класс
// вызов собственного findClass для попытки загрузки класса
c = findClass(name);
// ...
}
}
return c;
}
}

Как видно из приведенного выше кода, loadClass() сначала проверяет, был ли класс загружен ранее, используя findLoadedClass() (класс ClassLoader отслеживает ранее загруженные классы, чтобы предотвратить путаницу в системе, которая может возникнуть, если один и тот же класс будет загружен несколько раз). Если класс не был загружен ранее, он просит родительский загрузчик классов о загрузке, вызывая loadClass() родителя. Если родительский загрузчик не может загрузить класс (возвращает ClassNotFoundException), то он вызывает свой findClass() для попытки загрузить класс. Если findClass() не может возвратить объект Class, то выбрасывает ClassNotFoundException.

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

public Class load (String name) throws ClassNotFoundException {
//получение объекта загрузчика класса приложения
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//загрузка класса с помощью его имени
Class cls = classLoader.loadClass(name);
return cls;
}

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

public class CustomClassLoader extends ClassLoader{ /*..*/ }
ClassLoader cl = new CustomClassLoader(dir);
Class cls= cl.loadClass( "[class-name]" );

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

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

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

  • findClass() переопределяется при необходимости загрузки классов из новых мест;
  • loadClass() переопределяется, когда необходимо изменить модель делегирования или другое поведение по умолчанию в системе загрузки классов.

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

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Habiba Gamil: A Deep Dive into ClassLoader and Reflection — Dynamic Typing and Runtime Modifiable Classes in Java

Предыдущая статьяКак с легкостью создать установщик пакетов Android
Следующая статьяКомпиляция TypeScript в нативный код