Загрузчики классов Java — приведение класса к интерфейсу

#java #exception #plugins #interface #classloader

#java #исключение #Плагины #интерфейс #загрузчик классов

Вопрос:

Я пытаюсь разработать Java-приложение, в котором пользователи могут добавлять функциональность (с помощью плагинов), которая должна реализовывать общий интерфейс: PluginFunction. Затем пользовательские файлы .class должны находиться в указанном каталоге (в данном случае ./plugins) и загружаться моим пользовательским загрузчиком классов.

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

 java.lang.ClassCastException: class operaciones.Suma cannot be cast to class operaciones.PluginOperacion (operaciones.Suma is in unnamed module of loader logica.PluginClassLoader @daf4db4; operaciones.PluginOperacion is in unnamed module of loader java.net.URLClassLoader @6576fe71)
        at logica.OperacionesManager.loadOperaciones(OperacionesManager.java:75)
        at gui.commands.RefreshCommand.execute(RefreshCommand.java:28)
        at gui.GUI_CalcSimple.<init>(GUI_CalcSimple.java:124)
        at gui.GUI_CalcSimple$1.run(GUI_CalcSimple.java:60)
 

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

Спасибо

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

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

Ответ №1:

Примечание: Поскольку java представляет все типы типов (классы, интерфейсы, перечисления и т. Д.) java.lang.Class , Я буду использовать термин «класс» Для остальной части этого ответа, но он также охватывает интерфейсы и перечисления и тому подобное.

Обычно мы, разработчики Java, говорим, что тип (класс, интерфейс, перечисление и т. Д.) загружается системой только один раз (например, существует только один экземпляр java.lang.Class ).

Это верно.

Однако, «класс», это определение нуждается в некоторой корректировке: класс определяется не только его полным именем ( operaciones.Suma ) . На самом деле он определяется комбинацией его имени и его загрузчика.

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

Важно: совместимость экземпляров

Если вы каким-то образом получите 2 отдельных загруженных класса с именами, скажем, java.lang.Integer , , используя 2 загрузчика классов, каждый из которых индивидуально загружает этот класс, то эти типы полностью несовместимы. Вы получаете сумасшедшие ошибки, такие как «Экземпляр типа java.lang.Целое число не может быть присвоено переменной типа java.lang.Целое число».

Важно: понятие граничных типов

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

В системе плагинов это тоже имеет частные компоненты.

Но есть и третий мир: то, что находится между ними. Предположительно, в конечном итоге в вашем основном коде приложения будет сгенерирована строка, и вы передаете ее плагину. Это означает java.lang.String , что это граничный тип. Плагин имеет operaciones.PluginOperacion то, что ему нужно (в конце концов, он его реализует!), Но и ваш основной код тоже. Это тоже граничный тип.

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

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

Важно: «Что такое загрузчик?»

Что означает «загрузчик, который загрузил этот класс»? Это просто: загрузчики классов, в самом конце, вызывают собственный defineClass метод для себя, передавая байтовый массив байт-кода. ЭТО делает экземпляр, который вы вызываете defineClass , методом «загрузчика». Всякий раз, когда этому классу в конечном итоге требуется другой класс для выполнения своей работы, он немедленно попросит свой загрузчик классов сделать это. Это верно даже для чего-то такого простого, как java.lang.String .

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

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

  1. Загрузчики классов имеют родительские загрузчики.
  2. Чтобы загрузить любой ресурс, CL сначала попросит своих родителей загрузить его. Они вызывают defineClass , настраивая загруженный класс таким образом, чтобы ваш родительский элемент был загрузчиком, а не вашим пользовательским загрузчиком.
  3. Только если родитель (родители) не могут это сделать, CL сделает это сам. В итоге вы вызываете defineClass , теперь вы загрузчик. Любые типы, необходимые для того, что вы загрузили, начинаются с # 1 и снова приведут к «сначала спросите родителя, только если он не может, мы его загружаем».

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

Правильный дизайн

Итак, хитрость в том, чтобы использовать родительскую систему, чтобы гарантировать, что граничные типы загружаются только один раз. Возможно, с помощью одного загрузчика классов, который вы настроили для своего основного приложения, но это, вероятно, излишне: ваше основное приложение и ВСЕ граничные классы должны загружаться стандартным загрузчиком Java (который загружает класс с помощью вашего основного метода), а сам плагин и любые типы, которые он определяет, загружаются с помощьюзагрузчик плагина.

Из-за родительского происхождения это работает: у загрузчика плагина есть родительский элемент (основной загрузчик). Вы просите свой пользовательский загрузчик загрузить плагин. Сначала он запрашивает своего родителя (основного загрузчика), но тот не может его найти, потому что этого плагина нет в вашем пути к классу. Таким образом, pluginloader загружает его. В тот самый момент, когда pluginloader это сделал, pluginloader немедленно запрашивается для загрузки, operaciones.PluginOperacion потому что загружаемый вами класс плагина расширяет / реализует это.

Следуя стандартному назначению API, ваш pluginloader попросит своего родителя загрузить его, и … это должно быть успешным, и, таким образом j.l.Class , экземпляр, возвращаемый вашим pluginloader, тем не менее загружается mainloader. Как и в, вызов getClassLoader() этого вернет то же самое YourMainApp.class.getClassLoader() , что и возвращает.

Отлично! Как?

 class PluginLoader extends ClassLoader {
    public PluginLoader(ClassLoader parent) {
        super(parent);
    }

    public Class<?> findClass(String name) {
        String tgt = name.replace(".", "/")   ".class";
        byte[] bytecode = readFullyFromPluginjar(tgt);
        return defineClass(name, bytecode, 0, bytecode.length);
    }
}
 

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

Но обратите внимание, что сама java будет вызывать loadClass , а не findClass . К счастью, impl loadClass , который вы наследуете, сначала запросит parent, и только если parent не сможет найти, он вызовет findClass (что в конечном итоге приведет к запуску переопределенного кода выше). Таким образом, если вы хотите написать загрузчик классов, который не соответствует стандартному намерению сначала запросить parent , вместо этого вы переопределяете loadClass . Но обычно стандартное намерение — это то, что вы хотите, поэтому обычно переопределяйте FindClass , а не loadClass .

Чтобы использовать:

 class Main {
    public PluginOperaciones loadPlugin(Path jarLocation, String className) {
        PluginLoader loader = new PluginLoader(Main.class.getClassLoader());
        loader.setJarSearchSpace(jarLocation);
        Class<?> pl = loader.loadClass(className); // load, not find!!
        return (PluginOperaciones) pl.getConstructor().newInstance();
    }
}
 

Удачи с остальной частью проекта!

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

1. Или просто используйте a URLClassLoader вместо того, чтобы изобретать его заново…