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

Мне нужно добавить функциональность плагина в существующее приложение для определенных частей приложения. Я хочу иметь возможность добавить 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 ответов

Решение

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

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

Как указал Райан, в 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 который мог решить эту проблему. Я не проверял это.

Начиная с 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, вот мой взлом 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

Это обновление протестировано на 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"и другой для вашего реального приложения без зависимости между ними, или вы не сможете понять, кто и что загружает.

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

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

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

  1. В принципе, уже загруженный класс нельзя перезагрузить с тем же загрузчиком классов.
  2. Для новой загрузки необходимо создать новый загрузчик классов и, таким образом, загрузить класс.
  3. С помощью URLClassLoader есть одна проблема: файл jar остается открытым.
  4. Если у вас есть несколько классов, загруженных из одного файла jar разными экземплярами URLClassLoader и вы меняете файл jar во время выполнения, вы обычно получаете эту ошибку: java.util.zip.ZipException: ZipFile invalid LOC header (bad signature). Ошибка может быть другой.
  5. Чтобы вышеперечисленные ошибки не возникали, необходимо использовать close метод на всех URLClassLoaders, используя указанный файл jar. Но это решение, которое фактически приводит к перезапуску всего приложения.

Лучшее решение - изменить URLClassLoaderтак что содержимое файла jar загружается в кеш RAM. Это больше не влияет на другиеURLClassloaderкоторые читают данные из того же файла jar. Затем файл jar можно свободно изменять во время работы приложения. Например, вы можете использовать эту модификациюURLClassLoaderдля этого: в памяти URLClassLoader

Другие вопросы по тегам