Jar Hell: как использовать загрузчик классов для замены одной версии библиотеки JAR на другую во время выполнения - PullRequest
29 голосов
/ 02 августа 2011

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

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

  • Библиотека 1 требует именно версию 1 библиотеки 3.
  • Библиотека 2 требует именно версию 2 Библиотеки 3.

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

Вот пример структуры приложения, которая демонстрирует проблему. Класс Main (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 обе имеют взаимную зависимость от Library3, но Library1 требует именно версию 1, а Library2 требует именно версию 2. В примере обе эти библиотеки просто распечатывают версию Library3, которую они видят:

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 (требуется для библиотеки2):

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

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

Что-то, что мне нужно сделать, это заменить версию Library3, которая появляется на пути к классам перед созданием экземпляра 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's 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.» быть напечатанным. Поскольку это версия Library3, которая находится в пути к классам при запуске приложения, это ожидается.

Однако я ожидал, что lib2.bar() напечатает «Это версия 2.», отражая, что новая версия Library3 загружена, но все равно печатает «Это версия 1».

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

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

Ответы [ 7 ]

7 голосов
/ 20 декабря 2015

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

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

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

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

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

2 голосов
/ 22 мая 2012

Вам необходимо загрузить обе библиотеки Library1 и Library2 в отдельные загрузчики URLClass. (В вашем текущем коде 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();
1 голос
/ 02 августа 2011

Попытка избавиться от 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.

0 голосов
/ 30 декабря 2015

Я бы предложил решение, используя 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());

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

И затем вы можете создать модуль для 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());

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

    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]);

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

0 голосов
/ 05 февраля 2014

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

0 голосов
/ 26 июня 2012

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

0 голосов
/ 22 мая 2012

загрузчик классов является чем-то простым в концепции, но на самом деле довольно сложным

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

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

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

...