Программирование JavaJVM и памятьJava-разработчик серверных приложений

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

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

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

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

Исторический контекст

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

За счёт этого приложение может изолировать версии библиотек и классы разных плагинов. При этом JVM не рассматривает classpath как единую глобальную область видимости: каждый загрузчик формирует собственное пространство имён.

Постановка проблемы

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

Ошибкой будет считать, что наличие файла класса в процессе или доступность его через другой загрузчик гарантирует успешную загрузку. На практике это приводит к ClassNotFoundException при поиске класса либо к NoClassDefFoundError, если зависимость не удалось разрешить после первоначальной загрузки.

Подробное решение

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

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

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

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

Ситуация из практики

В сервере два плагина используют разные версии одной библиотеки. Плагин A передаёт плагину B объект реализации библиотеки напрямую, после чего B не может разрешить тип или получает ошибку приведения. Причина — реализация была загружена загрузчиком A, а загрузчик B работает в другом пространстве имён.

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

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

Что кандидаты часто упускают

  1. Достаточно ли общего полного имени класса для совместимости?

    Нет. Идентичность типа определяется парой «полное имя плюс определяющий загрузчик». Поэтому два класса com.example.Service, загруженные разными загрузчиками, являются разными типами. Объект одного из них нельзя безопасно привести к типу другого даже при побайтово одинаковом содержимом классов.

  2. Может ли дочерний загрузчик использовать класс родителя, если тот уже загрузил его?

    Обычно да: при родительской делегации дочерний загрузчик сначала обращается к родителю. Поэтому общие API и платформенные классы обычно размещают в родительском пространстве. Обратное направление не работает: родитель не ищет классы в дочернем загрузчике.

  3. Почему изменение контекстного загрузчика потока иногда помогает библиотеке, но не решает общую проблему видимости?

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