Плагинное приложение на Java без боли

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

    • Нам не понадобятся специальные библиотеки или фреймворки (OSGi, Guice и т.п.)
    • Мы не будем использовать парсинг байткода с помощью ASM и подобных библиотек.
    • Не будем писать свой загрузчик классов.
    • Не будем использовать рефлексию и аннотации.
    • Не понадобится возня с classpath для поиска плагинов. Мы вообще не будем трогать classpath.
    • Также мы не будем использовать XML, YAML или какие-либо другие декларативные языки для описания точек расширений (extension point’ов в плагинах).

    Однако всё же есть одно требование — такое решение будет работать только на Java 9 или выше. Потому что оно будет основано на модулях и сервисах.

    Итак, начнём. Сформулируем задачу более конкретно:
    Нужно реализовать минимальный каркас приложения, которое при старте будет подгружать пользовательские плагины из папки plugins.

    То есть собранное приложение должно выглядеть примерно так:

    plugin-app/
        plugins/
            plugin1.jar
            plugin2.jar
            ...
        core.jar
        …

    Давайте начнём с модуля core. Данный модуль — это ядро нашего приложения, то есть по сути и есть наш каркас.

    Для тех, кому дорого время, готовый проект выложен на GitHub. Инструкции по сборке.
    Ссылка

    git clone https://github.com/orionll/plugin-app
    cd plugin-app
    mvn verify
    cd core/target
    java --module-path core-1.0-SNAPSHOT.jar --module core

    Создадим в модуле следующие 4 Java-файла:

    core/
        src/main/java/
            org/example/pluginapp/core/
                IService.java
                BasicService.java
                Main.java
            module-info.java

    Первый файл, IService.java — это файл, в котором описана наша точка расширения. В эту точку расширения потом смогут делать вклад («контрибьютить») другие плагины. Это стандартный принцип построения плагинных приложений, который называется принципом инверсии зависимостей (Dependency Inversion). В основе этого принципа лежит то, что ядро зависит не от конкретных классов, а от интерфейсов.

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

    Файл IService.java выглядит следующим образом:

    …
    
    public interface IService {
       void doJob();
    
       static List<IService> getServices(ModuleLayer layer) {
           return ServiceLoader
                   .load(layer, IService.class)
                   .stream()
                   .map(Provider::get)
                   .collect(Collectors.toList());
       }
    }

    Таким образом, IService — это просто интерфейс, который делают некую абстрактную работу doJob() (повторюсь, детали не важны, в реальности это будет что-то конкретное).

    Также обратите внимание на второй метод getServices(). Этот метод возвращает все реализации интерфейса IService, который он нашёл в данном слое модулей и его родителях. Об этом мы поговорим подробнее чуть позже.

    Второй файл, BasicService.java — это базовая реализация интерфейса IService. Она будет всегда присутствовать, даже если в приложении не будет ни одного плагина. Другими словами, core — это не только ядро, но ещё и одновременно плагин для самого себя, который будет всегда загружен. Файл BasicService.java выглядит следующим образом:

    …
    
    public class BasicService implements IService {
       @Override
       public void doJob() {
           System.out.println("Basic service");
       }
    }

    Для простоты doJob() просто печатает строку "Basic service" и всё.

    Таким образом, на текущий момент мы имеем следующую картину:



    Третий файл, Main.java — это то, где реализован метод main(). В нём есть есть немножечко магии, для понимания которой нужно знать, что такое слой модулей (module layer).

    Про слои модулей


    Когда Java запускает приложение, то все модули платформы + модули, перечисленные в аргументе --module-path (и ещё classpath, если он есть), попадают в так называемый слой Boot. В нашем случае, если мы соберём модуль core.jar и запустим из командной строки java --module-path core.jar --module core, то в слое Boot будут как минимум модули java.base и core:



    Слой Boot всегда присутствует в любом Java-приложении, и это самая минимально возможная конфигурация. Большая часть приложений так и существует в одном единственном слое модулей. Однако в нашем случае мы хотим делать динамическую загрузку плагинов из папки plugins. Мы могли бы просто заставить пользователя исправлять строку запуска приложения, чтобы он сам добавлял в --module-path необходимые плагины, но это будет не самым лучшим решением. Особенно оно не понравится тем людям, которые не являются программистами и не понимают, зачем для такой простой вещи им нужно куда-то лезть и что-то исправлять.

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



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

    Итак, зная теперь, что такое слой модулей, наконец, можно посмотреть на содержимое файла Main.java:

    …
    
    public final class Main {
       public static void main(String[] args) {
           Path pluginsDir = Paths.get("plugins");
    
           // Будем искать плагины в папке plugins
           ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir);
    
           // Пусть ModuleFinder найдёт все модули в папке plugins и вернёт нам список их имён
           List<String> plugins = pluginsFinder
                   .findAll()
                   .stream()
                   .map(ModuleReference::descriptor)
                   .map(ModuleDescriptor::name)
                   .collect(Collectors.toList());
    
           // Создадим конфигурацию, которая выполнит резолюцию указанных модулей (проверит корректность графа зависимостей)
           Configuration pluginsConfiguration = ModuleLayer
                   .boot()
                   .configuration()
                   .resolve(pluginsFinder, ModuleFinder.of(), plugins);
    
           // Создадим слой модулей для плагинов
           ModuleLayer layer = ModuleLayer
                   .boot()
                   .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader());
    
           // Найдём все реализации сервиса IService в слое плагинов и в слое Boot
           List<IService> services = IService.getServices(layer);
           for (IService service : services) {
               service.doJob();
           }
       }
    }

    Если вы первый раз смотрите на этот код, то он может показаться очень сложным, но это ложное ощущение из-за большого количества новых неизвестных классов. Если немного разобраться в смысле классов ModuleFinder, Configuration и ModuleLayer, то всё становится на свои места. И кроме того, здесь всего лишь несколько десятков строк! Это вся логика, которая пишется один раз.

    Дескриптор модуля


    Остался ещё один (четвёртый) файл, который мы не рассмотрели: module-info.java. Это самый короткий файл, в котором содержатся декларация нашего модуля и описание сервисов (точек расширения):

    …
    
    module core {
       exports org.example.pluginapp.core;
    
       uses IService;
       provides IService with BasicService;
    }

    Смысл строк этого файла должен быть очевиден:

    • Во-первых, модуль экспортирует пакет org.example.pluginapp.core, чтобы плагины могли наследоваться от интерфейса IService (иначе IService не был бы доступен вне модуля core).
    • Во-вторых, он объявляет, что использует сервис IService.
    • В-третьих, он говорит, что предоставляет реализацию сервиса IService через класс BasicService.

    Так как декларация модуля написана на Java, то мы получаем очень важные преимущества: проверки компилятора и статические гарантии. Например, если бы мы ошиблись в названии типов или указали бы несуществующий пакет, то сразу получили бы по рукам. В случае какого-нибудь OSGi никаких проверок во время компиляции мы бы не имели, поскольку декларация точек расширения была бы написана на XML.

    Итак, каркас готов. Давайте попробуем запустить его:

    > java --module-path core.jar --module core
    Basic service

    Что произошло?

    1. Java попыталась найти модули в папке plugins и не нашла ни одного.
    2. Создался пустой слой.
    3. ServiceLoader начал поиск всех реализаций IService.
    4. В пустом слое он не нашёл ни одной реализации сервиса, поскольку там нет ни одного модуля.
    5. После этого слоя он продолжил поиск в родительском слое (т.е. слое Boot) и нашёл одну реализацию BasicService в модуле core.
    6. У всех найденных реализаций был вызван метод doJob(). Поскольку найдена только одна реализация, то было напечатано только "Basic service".

    Пишем плагин


    Написав ядро нашего приложения, теперь самое время попробовать написать к нему плагины. Давайте напишем два плагина plugin1 и plugin2: пусть первый печатает "Service 1", второй — "Service 2". Чтобы это сделать, нужно предоставить ещё две реализации IService в plugin1 и plugin2 соответственно:



    Создадим первый плагин с двумя файлами:

    plugin1/
        src/main/java/
            org/example/pluginapp/plugin1/
                Service1.java
            module-info.java

    Файл Service1.java:

    …
    
    public class Service1 implements IService {
       @Override
       public void doJob() {
           System.out.println("Service 1");
       }
    }

    Файл module-info.java:

    …
    
    module plugin1 {
       requires core;
    
       provides IService with Service1;
    }

    Обратите внимание, что plugin1 зависит от core. Это упоминаемый мною ранее принцип инверсии зависимостей: не ядро зависит от плагинов, а наоборот.

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

    Теперь давайте соберём плагины, положим их в папку plugins и запустим приложение:

    > java --module-path core.jar --module core
    Service 1
    Service 2
    Basic service

    Ура, плагины подхватились! Как это произошло:

    1. Java нашла два модуля в папке plugins.
    2. Создался слой с двумя модулями plugins1 и plugins2.
    3. ServiceLoader начал поиск всех реализаций IService.
    4. В слое плагинов он нашёл две реализации сервиса IService.
    5. После этого он продолжил поиск в родительском слое (т.е. слое Boot) и нашёл одну реализацию BasicService в модуле core.
    6. У всех найденных реализаций был вызван метод doJob().

    Заметьте, что именно потому, что поиск провайдеров сервисов начинается с дочерних слоёв, а потом переходит к родительским, то сначала печатается "Service 1" и "Service 2", а потом "Basic Service". Если хочется, чтобы сервисы были отсортированы так, чтобы сначала шли базовые сервисы, а потом плагины, то можно подправить метод IService.getServices(), добавив туда сортировку (возможно для этого придётся добавить метод int getOrdering() в интерфейс IService).

    Итоги


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

    • Простота: для точек расширения и их связывания используются только базовые возможности Java (интерфейсы, классы и ServiceLoader), без фреймворков, рефлексии, аннотаций и загрузчиков классов.
    • Декларативность: точки расширения описываются в дескрипторах модулей. Достаточно взглянуть на module-info.java и понять, какие существуют точки расширения и какие плагины делают вклад в эти точки.
    • Статические гарантии: в случае ошибок в дескрипторах модулей программа не скомпилируется. Также в качестве бонуса, если вы используете IntelliJ IDEA, то получаете дополнительные предупреждения (например, если забыли написать uses и при этом используете ServiceLoader.load())
    • Безопасность: модульная система Java проверяет во время старта, что конфигурация модулей является корректной, и в случае ошибок отказывается выполнять программу.

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

    Я решил поднять эту тему, потому что сам последние 7 лет пишу модульное приложение с использованием Eclipse RCP, в котором в качестве плагинной системы используется пресловутый OSGi, а дескрипторы плагинов пишутся на XML. У нас более сотни плагинов и мы пока что ещё сидим на Java 8. Но даже если мы перейдём на новую версию Java, то мы вряд ли будем использовать модули Java, поскольку сильно завязаны на OSGi.

    Но если вы пишете плагинное приложение с нуля, то модули Java — это один из возможных вариантов его реализации. Помните, что модули — это лишь инструмент, а не цель.

    Коротко обо мне


    Программирую более 10 лет (из них 8 на Java), отвечаю на StackOverflow и веду свой канал в Telegram, посвящённый Java.

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

    Какую версию Java используете?

    • 3,8%Java 6 или более старая версия4
    • 7,6%Java 78
    • 75,5%Java 880
    • 1,9%Java 92
    • 0,0%Java 100
    • 30,2%Java 1132
    • 11,3%Java 12+12
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 45

      +1
      Меня одного бесит бессмысленное использование буквы `I` в начале имени интерфейса? Почему нельзя интерфейс сервиса назвать просто `Service`?
        –2
        Хороший вопрос. Я считаю это очень хорошей практикой, потому что:
        • По имени типа сразу понятно, что это интерфейс
        • Если у интерфейса есть реализация, то её можно назвать просто Service, а не придумывать всякие ServiceImpl и т.д.

        Также это стандартная схема именования интерфейсов в коде Eclipse.
          +3
          Я считаю это очень хорошей практикой, потому что:

          Потому что писали когда-то на C#?
            –4
            Вы так говорите, как будто это что-то плохое
              +2
              Ну а чего не пошли дальше и не назвали методы с заглавной буквы, чего плохого-то? Есть общепринятые практики и решения, ничего против C# не имею, в рамках него есть свои правила написания кода и это замечательно. Раздражает, когда идут в чужой монастырь со своим уставом.
                –4
                Напомните, в какой официальной конвенции именования запрещены префиксы I? Требуется лишь только, чтобы имена типов были camel case и начинались с заглавной буквы. Про I там ничего не сказано.
            0
            Вообще-то, это шарп взял это из джавы)
              0

              Кто вам такое сказал?

            +2
            По имени типа сразу понятно, что это интерфейс


            И что это даёт?
            java.util.Map, java.util.List и java.util.Collection интерфейсы, там нет никаких IMap, IList, ICollection, читаемость от этого не хуже, при этом понимание, что это, есть. Создаётся впечатление, что здесь обозначаем, что это интерфейс ради того, чтобы обозначить, что тут интерфейс.

            Если у интерфейса есть реализация, то её можно назвать просто Service, а не придумывать всякие ServiceImpl и т.д.


            ИМХО, реализацию называть Service и/или ServiceImpl — плохая идея. Это как ArrayList обозвать ListImpl, т. е. имя класса ни о чём не говорит. А по ArrayList понятно, какая это реализация. Соответственно, реализации можно назвать, чтобы они отображали основной мотив реализации. Например, какие названия лучше выглядят:
            com.app.plugin1.ServiceImpl, com.app.plugin2.ServiceImpl, com.app.plugin3.ServiceImpl

            или
            com.app.plugin.KafkaService, com.app.plugin2.StubService, com.app.plugin2.MQService

            ?

            Также это стандартная схема именования интерфейсов в коде Eclipse.


            Поправьте меня, пожалуйста, но я не видел, чтобы в коде OpenJDK или Spring-а было обозначение интерфейса через начальную букву «I».
              –1
              Да, схема именования везде разная. В OpenJDK не используется I*, а вот в коде Eclipse используется:

              image
              Очень часто у интерфейса есть только один наследник, которому придётся придумывать новое имя, если бы префикс I не использовался. Вот какое новое имя вы бы дали Workbench или ViewSite?
                0
                DefaultWorkbench, DefaultViewSite?
                  0
                  DefaultWorkbench или даже SWTWorkbench потому, что конкретная реализация использует SWT для org.eclipse.ui.Workbench.

                  Аналогично для ViewSite, почему бы не SWTViewSite или DefaultViewSite? То, что класс называется Workbench не говорит ровным счётом ничего. А SWTWorkbench подсказывает, что это реализация на базе SWT. А потом появится SwingWorkbench. И понятно в общих чертах, что там.
                0
                Также это стандартная схема именования интерфейсов в коде Eclipse.

                Раз уж вы убегаете от Eclipse, то убегайте и от его схемы именования классов. Попробуйте что-то новое.

                  0
                  Я не убегаю от Eclipse никуда, с чего вы это вообще взяли?
                    0

                    Прочитал в первом же параграфе вашей статьи. OSGI — моль, Guice — боль, Java modules — соль.

                      0
                      Eclipse != OSGi. Мы много чего используем готового из Eclipse, чему просто не существует альтернатив. А OSGi — лишь крошечный кусочек.
                0

                Нет, не одного :) Но всё же это не лишено смысла, ведь это венгерская нотация, которая появилась не на пустом месте. Вы не поймёте что за класс внутри файла, пока не откроете его, если имя файла не начинается с I.

                  +1
                  Все enum начинать с E?
                    +1

                    За E уже зарезервировано EMF: EObject, EList ну и так далее.

                  +1

                  На тему именования хорошее обсуждение на SO: https://stackoverflow.com/questions/2814805/java-interfaces-implementation-naming-convention

                    0
                    Хорошее обсуждение, которое полностью основано на личных мнениях людей. «Я предпочитаю», «Я думаю» и всё в таком духе. Совершенно правильно, что вопрос закрыли.
                  –1

                  del

                    +1
                    Мы не будем использовать парсинг байткода с помощью ASM и подобных библиотек.

                    А нужно ли при загрузки плагинов читать байткод? Это используется в каких нибудь инструментах?
                      +1
                      Ну, понятно что личное мнение тут не показатель, но за последние 10 лет я такого ни разу не видел. Для загрузки плагина ничего из того, от чего автор отказался, по большей части не нужно — а нужен стандартный класслоадер, например URLClassLoader, и все. А уж все остальное — исключительно для других целей (например, тот же OSGI, загрузив бандл, сделает много чего полезного — например, создаст и инициализирует сервисы, описанные в бандле, передаст ссылки на них в существующие бандлы, которые подписаны на такие сервисы, и сообщит им об этом). То есть такого, что в описанном тут методе либо не предусмотрено, либо даже не реализуемо. А при помощи ASM, например, что-то в загруженных классах поменяет. Это не всегда нужно — но если нужно, то обычно без манипуляции байткодом не обойтись.
                        0
                        Вот мне тоже показалось что работа с байткодом тут преувеличена. Но если есть такие примеры, то было бы интересно взглянуть
                          0
                          >Вот мне тоже показалось что работа с байткодом тут преувеличена.
                          Ну я могу представить, для чего она может быть нужна. Скажем в OSGI, если вы импортируете чужой сервис, то контейнер вам подсунет на самом деле proxy класс, и если сервиса реально нет — то этот proxy кинет исключение. И прокси эти с некоторой вероятностью генерируются путем манипуляции с байткодом. Но это не точно.
                          0
                          Не совсем понимаю, как URLClassLoader поможет вам найти нужные реализации сервисов. Загрузчик классов может только загружать классы и всё. Будете грузить вообще все классы, которые есть, а потом через reflection искать нужные? У вас приложение будет стартовать по 10 минут.
                            0
                            >Будете грузить вообще все классы, которые есть
                            В случае плагина в отдельном jar все равно все так и будет. А иначе зачем нужно все, что вы туда пакуете?

                            >загрузчик классов может только загружать классы и всё.
                            Не все. Еще он может вернуть нам список пакетов, например.

                            В целом, я так скажу — соглашения об именах вполне могут эту проблему решить. Ведь если у вас плагин == класслоадер, все классы в нем вполне могут называться точно так же, как в ядре. Или в другом плагине. Так что вы просто создаете класслоадер на jar, и грузите из него класс с известным именем. Я совершенно не утверждаю при этом, что это удобно или эффективно. Скорее всего — ни то, ни другое.

                            А практически же — достаточно метаданных в хорошо всем известном месте — и получаете широко и давно известное решение а-ля ServiceLoader. Либо ровно его же, либо что-то свое.
                              0
                              В случае плагина в отдельном jar все равно все так и будет
                              Почему? ClassLoader в джаве грузит классы лениво, в момент первого обращения к ним. В джарке могут быть сотни классов и далеко не все из них понадобятся. Например, пользователь вообще не будет трогать те или иные кнопки, и тогда получается, что вы зря грузили все эти лишние классы.
                          0
                          Используется, и ни в каких-нибудь, а в Спринге! А как вы думаете там работают всякие автофигурации, когда вам достаточно просто навесить аннотации на типы, а Спринг сам просканирует classpath и найдёт их? Как вы это предлагаете делать без парсинга байткода? Если у вас нет XML-конфигурации, то список классов заранее неизвестен, и Спрингу придётся пропарсить классы, чтобы понять, какие именно нужны.
                            +1

                            Порой приходится, если описание конфигурации плагина, необходимой для его загрузки, записано, допустим, в аннотациях в некоторых классах плагина, однако при этом нужно избегать их загрузки.
                            Например, некая аннотация, указываются на то, какую зависимость нужно загрузить до данной или, например, просто указывающая на entry-point плагина.

                            +1

                            Всю жизнь для подобного использовали:


                            java -cp "core.jar;plugins/*" Main

                            а для lookup-а сервисов — ServiceLoader.


                            Мне кажется, это еще одна попытка оправдать бесполезность и рудиментарность модульной системы в Java 9. Самый большой ее недостаток — это привязка модулей к бинарным бандлам (jar) и соответвтенно лейауту проекта. Идея classpath проще и универсальней.

                              0
                              Идея иерархии класслоадеров еще универсальнее. А classpath в чистом виде я наверное уже лет 10 как почти не вижу. А везде где вижу — стараюсь отказываться.
                                0
                                а для lookup-а сервисов — ServiceLoader.
                                Вот только вам придётся для этого прописывать все свои реализации сервисов в `META-INF/services/`. Это большой геморрой, особенно когда точек расширений у вас очень много. Спасибо, но это слишком неудобно. Использовать module-info.java гораздо проще и надёжнее.

                                Идея classpath проще и универсальней.
                                Проще, но с ней вы теряете инкапсуляцию и надёжность графа зависимостей. Если у вас есть какая-нибудь ошибка вроде цикла или split-пакетов, то classpath вам ничего об этом не скажет.
                                  0
                                  Ну выж понимаете, что у этого решения зато есть другие преимущества — оно работает не только в Java 9 :)
                                    0
                                    Вот только вам придётся для этого прописывать все свои реализации сервисов в META-INF/services/.

                                    Для особо ленивых как-то так: http://metainf-services.kohsuke.org/
                                    Ну или как альтернатива для рантайма: https://github.com/ronmamo/reflections


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

                                    Люди не вчера зависимости придумали. Maven, Gradle генерят стабильный classpath, плюс позволяют разруливать конфликты. Никаких циклов не может быть — мавен юзает либу наибольшей версии, градл — ту, которая ближе к руту. Для особо дотошных есть dependency tree. Java modules:


                                    1. привязаны к бинарным артефактам, что делает ваш проект зависимым от способа пакетации ваших классов
                                    2. не решает проблем изолирования транзитивных зависимостей, как osgi
                                    3. шатают и вертят всю существовавшую до этого экосистему
                                    4. ничего реально полезного не привносят, кроме дополнительных ограничений и абстракций
                                      0
                                      Это же не мне ответ был?
                                        0

                                        Нее, пардон. Уровнем выше (автору).

                                +1
                                Очень много мануалов как загрузить класс.
                                А вот как можно выгрузить класс (чтоб загрузить его изменённый вариант). Например замена плагинов без рестарта всего ПО?
                                Или это в корне неправильно?
                                  0
                                  Выгрузить класс может только сборщик мусора, если класс станет недостижимым. Это значит, что не должно быть ни одной цепочки сильных ссылок от GC roots до любого объекта этого класса + не должно быть цепочек ссылок на сам этот класс. Но даже если вы всё это проконтролируете, то всё равно не сможете положиться на немедленную выгрузку классов, так как сборщик мусора собирает мусор непредсказуемо. Это значит, что какое-то время эти классы будут висеть в metaspace и занимать место.
                                  Загружать новые версии плагинов можно, и я даже встречал людей, которые утверждали, что это делают, но я считаю, что геморрой от этого превышает пользу.
                                  (Это всё моё личное мнение)
                                    0
                                    А какие тогда есть решения задачи «заменить jar-файл без рестарта „родительского“ приложения?
                                    (Естественно, интерфейсы в классах этого плагина не меняются, только логика).

                                    Apache Tomcat ведь без рестарта загружает/выгружает war-ники?
                                      +1
                                      Ещё раз: 100% надёжного решения нет. Даже в случае Томката.
                                        0
                                        Строго говоря — для этого нужно прибить именно класслоадер. У OSGI это получается, но это не слишком простое решение (не для пользователя, а скорее внутри).
                                      0

                                      Из нынешних решений только два пути:


                                      • неэффективный — один ClassLoader на группу классов, которая может быть отгружена. В случае с классами плагина это не столь критично, поскольку если и есть необходимость их отгрузки, то, скорее всего, всех вместе.
                                      • Unsafe-way: Unsafe#defineAnonymousClass(..) — способ, для которого, очевидно, тем более никто не даёт гарантий работоспособности и безопасности, но который более подходит для варианта, когда нужно загружать класс и отгружать, по возможности, как только у него нет экземпляров. Как пример, он используется для сгенерированный LambdaMetafactory и StringConcatFactory классов лямбд и соединителей строк, соответственно (актуально для текущей OpenJDK), а также в Nashorn для загрузки скомпилированных скриптов.

                                      На моём опыте, первый случай, как и было сказано, уместен для случаев, когда группа классов загружается и может быть в каком-то случае отгружена (eg всё те же плагины), а второй — для динамически генерируемых классов с жизненным циклом таким же, как и жизненный цикл их экземпляров (eg, я в одном из проектов для генерации динамических строк, на основе шаблонов, в рантайме генерирую класс с агрессивным инлайнингом, который, однако, нужен ровно столько, сколько существует его экземпляр).

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

                                    Самое читаемое