Подтвердить что ты не робот

Jar hell: как использовать загрузчик классов для замены одной версии библиотеки jar другим во время выполнения

Я все еще относительно новичок в Java, поэтому, пожалуйста, несите меня.

Моя проблема в том, что мое приложение Java зависит от двух библиотек. Позвольте называть их библиотекой 1 и библиотекой 2. Обе библиотеки имеют общую зависимость от библиотеки 3. Однако:

  • Для библиотеки 1 требуется точно версия 1 библиотеки 3.
  • Библиотека 2 требует ровно версии 2 библиотеки 3.

Это как раз определение JAR hell (или хотя бы один его вариант). Как указано в ссылке, я не могу загрузить обе версии третьей библиотеки в один и тот же загрузчик классов. Таким образом, я пытался выяснить, могу ли я создать новый загрузчик классов в приложении для решения этой проблемы. Я смотрел URLClassLoader, но я не смог понять это.

Вот пример структуры приложения, которая демонстрирует проблему. Основной класс (Main.java) приложения пытается создать экземпляр как Library1, так и Library2 и запустить некоторый метод, определенный в этих библиотеках:

Main.java(оригинальная версия перед любой попыткой решения):

public class Main {
    public static void main(String[] args) {
        Library1 lib1 = new Library1();
        lib1.foo();

        Library2 lib2 = new Library2();
        lib2.bar();
    }
}

Library1 и Library2 имеют общую зависимость от библиотеки3, но для библиотеки 1 требуется точно версия 1, а для библиотеки 2 требуется точно версия 2. В этом примере обе эти библиотеки просто распечатывают версию библиотеки3, которую они видят:

Library1.java:

public class Library1 {
  public void foo() {
    Library3 lib3 = new Library3();
    lib3.printVersion();    // Should print "This is version 1."
  }
}

Library2.java:

public class Library2 {
  public void foo() {
    Library3 lib3 = new Library3();
    lib3.printVersion();    // Should print "This is version 2." if the correct version of Library3 is loaded.
  }
}

И тогда, конечно, есть несколько версий Library3. Все, что они делают, это распечатать номера своих версий:

Версия 1 библиотеки3 (требуется для библиотеки 1):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 1.");
  }
}

Версия 2 библиотеки3 (требуется Library2):

public class Library3 {
  public void printVersion() {
    System.out.println("This is version 2.");
  }
}

Когда я запускаю приложение, путь к классу содержит Library1 (lib1.jar), Library2 (lib2.jar) и версию 1 библиотеки 3 (lib3-v1/lib3.jar). Это отлично работает для Library1, но не будет работать для Library2.

Что-то мне нужно сделать, это заменить версию библиотеки3, которая появляется в пути к классам, прежде чем создавать экземпляр Library2. У меня создалось впечатление, что для этого можно использовать URLClassLoader, вот что я пробовал:

Main.java(новая версия, включая мою попытку решения):

import java.net.*;
import java.io.*;

public class Main {
  public static void main(String[] args)
    throws MalformedURLException, ClassNotFoundException,
          IllegalAccessException, InstantiationException,
          FileNotFoundException
  {
    Library1 lib1 = new Library1();
    lib1.foo();     // This causes "This is version 1." to print.

    // Original code:
    // Library2 lib2 = new Library2();
    // lib2.bar();

    // However, we need to replace Library 3 version 1, which is
    // on the classpath, with Library 3 version 2 before attempting
    // to instantiate Library2.

    // Create a new classloader that has the version 2 jar
    // of Library 3 in its list of jars.
    URL lib2_url = new URL("file:lib2/lib2.jar");        verifyValidPath(lib2_url);
    URL lib3_v2_url = new URL("file:lib3-v2/lib3.jar");  verifyValidPath(lib3_v2_url);
    URL[] urls = new URL[] {lib2_url, lib3_v2_url};
    URLClassLoader c = new URLClassLoader(urls);

    // Try to instantiate Library2 with the new classloader    
    Class<?> cls = Class.forName("Library2", true, c);
    Library2 lib2 = (Library2) cls.newInstance();

    // If it worked, this should print "This is version 2."
    // However, it still prints that it version 1. Why?
    lib2.bar();
  }

  public static void verifyValidPath(URL url) throws FileNotFoundException {
    File filePath = new File(url.getFile());
    if (!filePath.exists()) {
      throw new FileNotFoundException(filePath.getPath());
    }
  }
}

Когда я запускаю это, lib1.foo() вызывает "Это версия 1." для печати. Поскольку это версия библиотеки3, которая на пути к классу при запуске приложения, это ожидается.

Однако я ожидал, что lib2.bar() напечатает "This is version 2.", что отражает загрузку новой версии Library3, но она все еще печатает "Это версия 1".

Почему использование нового загрузчика классов с правильной загруженной версией jar-версии приводит к использованию старой версии jar? Я делаю что-то неправильно? Или я не понимаю концепцию загрузчиков классов? Как я могу корректно переключать jar-версии библиотеки3 во время выполнения?

Буду признателен за любую помощь по этой проблеме.

4b9b3361

Ответ 1

Я не могу поверить, что уже более 4 лет никто не ответил на этот вопрос правильно.

https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html

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

Сергей, проблема с вашим примером заключалась в том, что библиотека 1,2 и 3 находились в пути класса по умолчанию, поэтому загрузчик классов Application, который был родителем вашего URLClassloder, смог загрузить классы из Библиотеки 1,2 и 3.

Если вы удалите библиотеки из пути к классам, загрузчик классов приложений не сможет разрешать классы из них, поэтому он делегирует разрешение его дочернему элементу - URLClassLoader. Вот что вам нужно делать.

Ответ 2

Вам нужно загружать библиотеки Library1 и Library2 в отдельные URLClassloaders. (В вашем текущем коде Library2 загружается в URLClassloader, чей родитель является основным загрузчиком классов, который уже загрузил Library1.)

Измените свой пример на что-то вроде этого:

URL lib1_url = new URL("file:lib1/lib1.jar");        verifyValidPath(lib1_url);
URL lib3_v1_url = new URL("file:lib3-v1/lib3.jar");  verifyValidPath(lib3_v1_url);
URL[] urls1 = new URL[] {lib1_url, lib3_v21_url};
URLClassLoader c1 = new URLClassLoader(urls1);

Class<?> cls1 = Class.forName("Library1", true, c);
Library1 lib1 = (Library1) cls1.newInstance();    


URL lib2_url = new URL("file:lib2/lib2.jar");        verifyValidPath(lib2_url);
URL lib3_v2_url = new URL("file:lib3-v2/lib3.jar");  verifyValidPath(lib3_v2_url);
URL[] urls2 = new URL[] {lib2_url, lib3_v2_url};
URLClassLoader c2 = new URLClassLoader(url2s);


Class<?> cls2 = Class.forName("Library2", true, c);
Library2 lib2 = (Library2) cls2.newInstance();

Ответ 3

Попытка избавиться от classpath lib2 и вызвать метод bar() путем отражения:

try {
    cls.getMethod("bar").invoke(cls.newInstance());
} catch (Exception e) {
    e.printStackTrace();
}

дает следующий результат:

Exception in thread "main" java.lang.ClassNotFoundException: Library2
    at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:247)
    at Main.main(Main.java:36)

Это означает, что вы фактически загружаете Library2 из classpath, используя загрузчик по умолчанию, а не свой собственный URLClassLoader.

Ответ 4

classloader - это что-то простое в концепции, но на самом деле довольно сложное

Я рекомендую вам не использовать специальное решение

у вас есть частичные решения с открытым исходным кодом, такие как DCEVM

но есть также очень хороший коммерческий продукт, например JRebel

Ответ 5

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

Ответ 6

Вы можете использовать ParentLastClassloader для решения Jar Hell. Пожалуйста, проверьте это сообщение в блоге вне

Ответ 7

Я предлагаю решение с помощью JBoss-Modules.

Вам нужно создать модуль для библиотеки 1:

    final ModuleIdentifier module1Id = ModuleIdentifier.fromString("library1");
    ModuleSpec.Builder moduleBuilder = ModuleSpec.build(module1Id);
    JarFile jarFile = new JarFile("lib/lib3-v1/lib3.jar", true);
    ResourceLoader rl1 = ResourceLoaders.createJarResourceLoader("lib3-v1", jarFile);
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            rl1
            ));
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            TestResourceLoader.build()
            .addClass(Library1.class)
            .create()
            ));
    moduleBuilder.addDependency(DependencySpec.createLocalDependencySpec());
    moduleLoader.addModuleSpec(moduleBuilder.create());

Аналогичным образом вы можете создать модуль для библиотеки 2.

И тогда вы можете создать модуль для Main, в зависимости от этих двух:

    //Building main module
    final ModuleIdentifier moduleMainId = ModuleIdentifier.fromString("main");
    moduleBuilder = ModuleSpec.build(moduleMainId);
    moduleBuilder.addResourceRoot(ResourceLoaderSpec.createResourceLoaderSpec(
            TestResourceLoader.build()
            .addClass(Main.class)
            .create()
            ));
    //note the dependencies
    moduleBuilder.addDependency(DependencySpec.createModuleDependencySpec(module1Id, true, false));
    moduleBuilder.addDependency(DependencySpec.createModuleDependencySpec(module2Id, true, false));
    moduleBuilder.addDependency(DependencySpec.createLocalDependencySpec());
    moduleLoader.addModuleSpec(moduleBuilder.create());

Наконец, вы можете загрузить основной класс и запустить его через отражение:

    Module moduleMain = moduleLoader.loadModule(moduleMainId);
    Class<?> m = moduleMain.getClassLoader().loadClass("tmp.Main");
    Method method = m.getMethod("main", String[].class);
    method.invoke(null, (Object) new String[0]);

Вы можете загрузить полный рабочий пример здесь