Проблема перезагрузки банки с помощью URLClassLoader

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

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

Я написал несколько примеров кода, но попал в исключение 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 в банку с именем test.jar и поместил его под C:\plugins и запустил этот код. Первая итерация выполняется плавно, и класс загружается без проблем.

Когда программа выполняет оператор sleep, я заменяю C:\plugins\test.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)

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

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

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

Ответы

Ответ 1

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

Ответ 2

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

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

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

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

Код для закрытия открытых файлов 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;
    }
}

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

Однако есть catch:. Чтобы этот код работал и имел возможность получить дескриптор открытых файлов 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, который, возможно, решил Эта проблема. Я не подтвердил это.

Ответ 3

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

Так что в этом конкретном случае все еще требуется взломать даже с Java 1.8.0_74, вот мой hack 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

Ответ 4

Это обновление, протестированное на 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();

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

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

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

Ответ 5

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

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

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