Проблема перезагрузки банки с помощью URLClassLoader - PullRequest
17 голосов
/ 10 июля 2010

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

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

Я написал некоторый пример кода, но нажал исключение NullPointerException.Сначала позвольте мне показать вам, ребята, код:

package test.misc;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

import plugin.misc.IPlugin;

public class TestJarLoading {

    public static void main(String[] args) {

        IPlugin plugin = null;

        while(true) {
            try {
                File file = new File("C:\\plugins\\test.jar");
                String classToLoad = "jartest.TestPlugin";
                URL jarUrl = new URL("jar", "","file:" + file.getAbsolutePath()+"!/");
                URLClassLoader cl = new URLClassLoader(new URL[] {jarUrl}, TestJarLoading.class.getClassLoader());
                Class loadedClass = cl.loadClass(classToLoad);
                plugin = (IPlugin) loadedClass.newInstance();
                plugin.doProc();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    Thread.sleep(30000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

IPlugin - простой интерфейс с одним методом doProc:

public interface IPlugin {
    void doProc();
}

, а jartest.TestPlugin - реализация этого интерфейса, где doProcпросто выводит некоторые операторы.

Теперь я упаковываю класс jartest.TestPlugin в jar с именем test.jar, помещаю его в C: \ plugins и запускаю этот код.Первая итерация проходит гладко, и класс загружается без проблем.

Когда программа выполняет оператор sleep, я заменяю C: \ plugins \ test.jar новым jar, содержащим обновленную версию того же класса, и жду следующей итерации while.Теперь вот что я не понимаю.Иногда обновленный класс перезагружается без проблем, т.е. следующая итерация проходит нормально.Но иногда я вижу выброшенное исключение:

java.lang.NullPointerException
at java.io.FilterInputStream.close(FilterInputStream.java:155)
at sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream.close(JarURLConnection.java:90)
at sun.misc.Resource.getBytes(Resource.java:137)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:256)
at java.net.URLClassLoader.access$000(URLClassLoader.java:56)
at java.net.URLClassLoader$1.run(URLClassLoader.java:195)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
at test.misc.TestJarLoading.main(TestJarLoading.java:22)

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

Мне нужен ваш опыт и знания, чтобы понять это.Что не так с этим кодом?Пожалуйста, помогите !!

Дайте мне знать, если вам нужна дополнительная информация.Спасибо за внимание!

Ответы [ 5 ]

14 голосов
/ 10 декабря 2010

Для всеобщего блага позвольте мне суммировать реальную проблему и решение, которое помогло мне.

Как отметил Райан, в JVM есть ошибка , которая влияет на платформу Windows. URLClassLoader не закрывает открытые файлы jar после того, как открывает их для загрузки классов, эффективно блокируя файлы jar. Файлы jar не могут быть удалены или заменены.

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

URLClassLoader -> URLClassPath ucp -> ArrayList<Loader> loaders
JarLoader -> JarFile jar -> jar.close()

Код для закрытия файлов open jar можно добавить в метод close () в классе, расширяющем URLClassLoader:

public class MyURLClassLoader extends URLClassLoader {

public PluginClassLoader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
}

    /**
     * Closes all open jar files
     */
    public void close() {
        try {
            Class clazz = java.net.URLClassLoader.class;
            Field ucp = clazz.getDeclaredField("ucp");
            ucp.setAccessible(true);
            Object sunMiscURLClassPath = ucp.get(this);
            Field loaders = sunMiscURLClassPath.getClass().getDeclaredField("loaders");
            loaders.setAccessible(true);
            Object collection = loaders.get(sunMiscURLClassPath);
            for (Object sunMiscURLClassPathJarLoader : ((Collection) collection).toArray()) {
                try {
                    Field loader = sunMiscURLClassPathJarLoader.getClass().getDeclaredField("jar");
                    loader.setAccessible(true);
                    Object jarFile = loader.get(sunMiscURLClassPathJarLoader);
                    ((JarFile) jarFile).close();
                } catch (Throwable t) {
                    // if we got this far, this is probably not a JAR loader so skip it
                }
            }
        } catch (Throwable t) {
            // probably not a SUN VM
        }
        return;
    }
}

(Этот код взят из второй ссылки, опубликованной Райаном. Этот код также размещен на странице отчета об ошибке.)

Однако есть одна загвоздка: Чтобы этот код работал и имел возможность получить дескриптор открытых файлов jar, чтобы закрыть их, загрузчик, используемый для загрузки классов из файла реализацией URLClassLoader, имеет быть JarLoader. Глядя на исходный код из URLClassPath (метод getLoader(URL url)), я заметил, что он использует JARLoader только в том случае, если строка файла, используемая для создания URL-адреса, не заканчивается на "/". Итак, URL должен быть определен так:

URL jarUrl = new URL("file:" + file.getAbsolutePath());

Общий код загрузки класса должен выглядеть примерно так:

void loadAndInstantiate() {
    MyURLClassLoader cl = null;
    try {
        File file = new File("C:\\jars\\sample.jar");
        String classToLoad = "com.abc.ClassToLoad";
        URL jarUrl = new URL("file:" + file.getAbsolutePath());
        cl = new MyURLClassLoader(new URL[] {jarUrl}, getClass().getClassLoader());
        Class loadedClass = cl.loadClass(classToLoad);
        Object o = loadedClass.getConstructor().newInstance();
    } finally {
        if(cl != null)
            cl.close();
    } 
}

Обновление: JRE 7 представила метод close() в классе URLClassLoader, который, возможно, решил эту проблему. Я не проверял это.

11 голосов
/ 10 июля 2010

Это поведение связано с ошибкой в jvm
Задокументированы 2 обходных пути здесь

2 голосов
/ 11 марта 2016

Начиная с Java 7, у вас действительно есть метод close() в URLClassLoader, но этого недостаточно для полного освобождения jar-файлов, если вы вызываете прямо или косвенно методы типа ClassLoader#getResource(String), ClassLoader#getResourceAsStream(String) или ClassLoader#getResources(String).Действительно, по умолчанию экземпляры JarFile автоматически сохраняются в кэше JarFileFactory в случае, если мы вызываем прямо или косвенно один из предыдущих методов, и эти экземпляры не освобождаются, даже если мы вызываем java.net.URLClassLoader#close().

* 1010.* Таким образом, взлом все еще необходим в данном конкретном случае, даже с Java 1.8.0_74, вот мой взлом https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/util/Classpath.java#L83, который я здесь использую https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L388. Даже с этим взломом мне все еще приходилось вызывать GCявно, чтобы полностью выпустить файлы JAR, как вы можете видеть здесь https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L419
1 голос
/ 25 июля 2015

Это обновление протестировано на Java 7 с успехом .Теперь URLClassLoader отлично работает для меня

MyReloader

class MyReloaderMain {

...

//assuming ___BASE_DIRECTORY__/lib for jar and ___BASE_DIRECTORY__/conf for configuration
String dirBase = ___BASE_DIRECTORY__;

File file = new File(dirBase, "lib");
String[] jars = file.list();
URL[] jarUrls = new URL[jars.length + 1];
int i = 0;
for (String jar : jars) {
    File fileJar = new File(file, jar);
    jarUrls[i++] = fileJar.toURI().toURL();
    System.out.println(fileJar);
}
jarUrls[i] = new File(dirBase, "conf").toURI().toURL();

URLClassLoader classLoader = new URLClassLoader(jarUrls, MyReloaderMain.class.getClassLoader());

// this is required to load file (such as spring/context.xml) into the jar
Thread.currentThread().setContextClassLoader(classLoader);

Class classToLoad = Class.forName("my.app.Main", true, classLoader);

instance = classToLoad.newInstance();

Method method = classToLoad.getDeclaredMethod("start", args.getClass());
Object result = method.invoke(instance, args);

...
}

Закройте и перезапустите ClassReloader

, затем обновите флягу и позвоните

classLoader.close();

затем вы можете перезапустить приложение с новой версией.

Не включайте свой jar в загрузчик базового класса

Не включайте свой jar в загрузчик базового класса "MyReloaderMain.class.getClassLoader()" из"MyReloaderMain", другими словами, разработайте 2 проекта с 2 банками один для "MyReloaderMain" и другой для вашего реального приложения без зависимости между ними, иначе вы не сможете понять, кто и что загружает.

0 голосов
/ 13 апреля 2016

Ошибка по-прежнему присутствует в jdk1.8.0_2 5 на Windows.Хотя ответ @Nicolas помогает, я нажимаю ClassNotFound для sun.net.www.protocol.jar.JarFileFactory при запуске его на WildFly, и несколько vm вылетает при отладке некоторых блочных тестов ...

Поэтому я в итоге извлек частькод, который занимается загрузкой и выгрузкой, на внешний сосуд.Из основного кода, который я просто называю с помощью java -jar...., на данный момент все выглядит нормально.

ПРИМЕЧАНИЕ. Windows освобождает блокировки загруженных файлов JAR при выходе из JVM, поэтому это работает.

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