java nullpointerexception urlclassloader

java - Problema al recargar un tarro utilizando URLClassLoader



nullpointerexception (5)

Necesito agregar funcionalidad de complemento a una aplicación existente para ciertas partes de la aplicación. Quiero poder agregar un jar en tiempo de ejecución y la aplicación debería poder cargar una clase desde el jar sin reiniciar la aplicación. Hasta ahora tan bueno. Encontré algunas muestras en línea usando URLClassLoader y funciona bien.

También quería la posibilidad de volver a cargar la misma clase cuando esté disponible una versión actualizada del jar. Nuevamente encontré algunos ejemplos y la clave para lograr esto, ya que entiendo es que necesito usar una nueva instancia de cargador de clases para cada nueva carga.

Escribí un código de ejemplo, pero golpeé una NullPointerException. Primero déjame mostrarles el código:

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 es una interfaz simple con solo un método doProc:

public interface IPlugin { void doProc(); }

y jartest.TestPlugin es una implementación de esta interfaz donde doProc simplemente imprime algunas declaraciones.

Ahora, empaqueté la clase jartest.TestPlugin en un jar llamado test.jar, lo coloco en C: / plugins y ejecuto este código. La primera iteración se ejecuta sin problemas y la clase se carga sin problemas.

Cuando el programa está ejecutando la instrucción de suspensión, sustituyo C: / plugins / test.jar por un nuevo archivo jar que contiene una versión actualizada de la misma clase y espero la siguiente iteración de while. Ahora esto es lo que no entiendo. A veces, la clase actualizada se vuelve a cargar sin problemas, es decir, la siguiente iteración funciona bien. Pero a veces, veo una excepción lanzada:

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)

Busqué en la red y me rasqué la cabeza, pero realmente no puedo llegar a ninguna conclusión sobre por qué se lanza esta excepción y eso también, solo a veces, no siempre.

Necesito tu experiencia y pericia para entender esto. ¿Qué hay de malo con este código? ¡¡Por favor ayuda!!

Déjame saber si necesitas más información. ¡Gracias por mirar!


A partir de Java 7, de hecho, tiene un método close() en URLClassLoader pero no es suficiente liberar completamente los archivos jar si llama directa o indirectamente métodos de tipo ClassLoader#getResource(String) , ClassLoader#getResourceAsStream(String) o ClassLoader#getResources(String) . De hecho, de forma predeterminada, las instancias de JarFile se almacenan automáticamente en el caché de JarFileFactory en caso de que llamemos directa o indirectamente a uno de los métodos anteriores y esas instancias no se liberen incluso si llamamos a java.net.URLClassLoader#close() .

Así que aún se necesita un hack en este caso particular, incluso con Java 1.8.0_74, aquí está mi hack https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/util/Classpath.java#L83 que uso aquí https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L388 . Incluso con este truco, todavía tuve que llamar al GC explícitamente para liberar completamente los archivos jar como se puede ver aquí https://github.com/essobedo/application-manager/blob/master/src/main/java/com/github/essobedo/appma/core/DefaultApplicationManager.java#L419


El error sigue presente en jdk1.8.0_2 5 en Windows . Aunque la respuesta de @Nicolas me ayuda, golpeo un ClassNotFound para sun.net.www.protocol.jar.JarFileFactory cuando lo sun.net.www.protocol.jar.JarFileFactory en WildFly, y varias vm se sun.net.www.protocol.jar.JarFileFactory al depurar algunas pruebas de caja ...

Por lo tanto, terminé extrayendo la parte del código que se ocupa de la carga y descarga, a un contenedor externo. Desde el código principal, solo lo llamo java -jar.... todo parece estar bien por ahora.

NOTA: Windows libera los bloqueos en los archivos jar cargados cuando sale el jvm, por eso funciona.


Esta es una actualización probada en Java 7 con éxito . Ahora el URLClassLoader funciona bien para mí

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); ... }

Cerrar y reiniciar el ClassReloader

luego actualiza tu jarra y llama

classLoader.close();

A continuación, puede reiniciar la aplicación con la nueva versión.

No incluya su tarro en su cargador de clase base

No incluya su jar en su cargador de clase base " MyReloaderMain.class.getClassLoader() " de " MyReloaderMain ", es decir, desarrolle 2 proyectos con 2 tarros uno para " MyReloaderMain " y el otro para su aplicación real sin dependencia entre los dos, o no podrás entender quién carga qué.


Este comportamiento está relacionado con un bug en el jvm.
2 soluciones están documentadas here


Para el beneficio de todos, permítame resumir el problema real y la solución que funcionó para mí.

Como Ryan señaló, hay un bug en JVM, que afecta a la plataforma Windows. URLClassLoader no cierra los archivos jar abiertos después de que los abre para cargar clases, bloqueando efectivamente los archivos jar. Los archivos jar no pueden ser eliminados o reemplazados.

La solución es simple: cierre los archivos jar abiertos después de haberlos leído. Sin embargo, para obtener un identificador de los archivos jar abiertos, debemos utilizar la reflexión, ya que las propiedades que debemos recorrer no son públicas. Así que recorremos este camino.

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

El código para cerrar los archivos jar abiertos se puede agregar a un método close () en una clase que extiende 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; } }

(Este código se tomó del segundo enlace que Ryan publicó. Este código también se publica en la página del informe de errores).

Sin embargo, hay un problema: para que este código funcione y pueda manejar los archivos jar abiertos para cerrarlos, el cargador utilizado para cargar las clases desde el archivo mediante la implementación de URLClassLoader tiene que ser un JarLoader . Al URLClassPath el código fuente de URLClassPath (método getLoader(URL url) ), noté que usa un JARLoader solo si la cadena de archivo utilizada para crear la URL no termina en "/". Entonces, la URL debe definirse así:

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

El código de carga de la clase en general debería verse así:

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(); } }

Actualización: JRE 7 ha introducido un método close() en la clase URLClassLoader que puede haber resuelto este problema. No lo he verificado.