example - java jar classpath command line
Jar hell: cómo usar un cargador de clases para reemplazar una versión de la biblioteca jar por otra en tiempo de ejecución (7)
Todavía soy relativamente nuevo en Java, así que por favor tengan paciencia conmigo.
Mi problema es que mi aplicación Java depende de dos bibliotecas. Llamémoslos Biblioteca 1 y Biblioteca 2. Ambas bibliotecas comparten una dependencia mutua en la Biblioteca 3. Sin embargo:
- La Biblioteca 1 requiere exactamente la versión 1 de la Biblioteca 3.
- La Biblioteca 2 requiere exactamente la versión 2 de la Biblioteca 3.
Esta es exactamente la definición del infierno JAR (o al menos una de sus variaciones). Como se indica en el enlace, no puedo cargar ambas versiones de la tercera biblioteca en el mismo cargador de clases. Por lo tanto, he estado tratando de averiguar si puedo crear un nuevo cargador de clases dentro de la aplicación para resolver este problema. He estado buscando en URLClassLoader , pero no he podido averiguarlo.
Aquí hay una estructura de aplicación de ejemplo que demuestra el problema. La clase principal (Main.java) de la aplicación intenta crear instancias de Library1 y Library2 y ejecuta algún método definido en esas bibliotecas:
Main.java (versión original, antes de cualquier intento de solución):
public class Main {
public static void main(String[] args) {
Library1 lib1 = new Library1();
lib1.foo();
Library2 lib2 = new Library2();
lib2.bar();
}
}
Library1 y Library2 comparten una dependencia mutua en Library3, pero Library1 requiere exactamente la versión 1, y Library2 requiere exactamente la versión 2. En el ejemplo, estas dos bibliotecas solo imprimen la versión de Library3 que ven:
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.
}
}
Y luego, por supuesto, hay múltiples versiones de Library3. Todo lo que hacen es imprimir sus números de versión:
Versión 1 de Library3 (requerida por Library1):
public class Library3 {
public void printVersion() {
System.out.println("This is version 1.");
}
}
Versión 2 de Library3 (requerida por Library2):
public class Library3 {
public void printVersion() {
System.out.println("This is version 2.");
}
}
Cuando inicio la aplicación, el classpath contiene Library1 (lib1.jar), Library2 (lib2.jar) y la versión 1 de Library 3 (lib3-v1 / lib3.jar). Esto funciona bien para Library1, pero no funcionará para Library2.
Lo que de alguna manera tengo que hacer es reemplazar la versión de Library3 que aparece en el classpath antes de crear una instancia de Library2. Tenía la impresión de que se podría usar URLClassLoader para esto, así que aquí está lo que probé:
Main.java (nueva versión, incluido mi intento de solución):
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());
}
}
}
Cuando ejecuto esto, lib1.foo()
causa "This is version 1." para ser impreso. Dado que esa es la versión de Library3 que está en el classpath cuando se inicia la aplicación, se espera esto.
Sin embargo, esperaba lib2.bar()
para imprimir "Esta es la versión 2.", lo que refleja que la nueva versión de Library3 se cargó, pero aún se imprime "Esta es la versión 1."
¿Por qué el uso del nuevo cargador de clases con la versión jar correcta cargada todavía da como resultado que se use la versión anterior de jar? ¿Estoy haciendo algo mal? ¿O no estoy entendiendo el concepto detrás de los cargadores de clases? ¿Cómo puedo cambiar las versiones de jar de Library3 correctamente en tiempo de ejecución?
Agradecería cualquier ayuda en este problema.
Tratando de deshacerse de classpath lib2
e invocar el método bar()
por reflexión:
try {
cls.getMethod("bar").invoke(cls.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
da el siguiente resultado:
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)
Esto significa que de hecho está cargando Library2
desde classpath
usando el cargador de classpath
predeterminado, no su URLClassLoader
personalizado.
Necesita cargar tanto Library1 como Library2 en URLClassloaders por separado. (En su código actual, Library2 se carga en un URLClassloader cuyo elemento principal es el cargador de clases principal, que ya ha cargado Library1).
Cambie su ejemplo a algo como esto:
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();
Utilice el cargador de clases jar que se puede utilizar para cargar clases de archivos jar en tiempo de ejecución.
Puedes usar ParentLastClassloader para resolver Jar Hell. Por favor revisa esta publicación del blog
No puedo creer que durante más de 4 años nadie haya respondido esta pregunta correctamente.
https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
La clase ClassLoader usa un modelo de delegación para buscar clases y recursos. Cada instancia de ClassLoader tiene un cargador de clases principal asociado. Cuando se le solicite que encuentre una clase o recurso, una instancia de ClassLoader delegará la búsqueda de la clase o recurso en su cargador de clases principal antes de intentar encontrar la clase o el recurso en sí. El cargador de clases incorporado de la máquina virtual, denominado "cargador de clases de arranque", no tiene un elemento primario, pero puede servir como elemento primario de una instancia de ClassLoader.
Sergei, el problema con su ejemplo fue que la Biblioteca 1,2 y 3 estaban en la ruta de clases predeterminada, por lo que el cargador de clases de la Aplicación que fue el padre de su clase URLClassloder pudo cargar las clases de la Biblioteca 1,2 y 3.
Si elimina las bibliotecas del classpath, el cargador de clases de la aplicación no podrá resolver las clases de ellas, por lo que delegará la resolución en su hijo: el URLClassLoader. Entonces eso es lo que necesitas hacer.
Sugeriría una solución usando JBoss-Modules
.
Solo necesita crear un módulo para Library1:
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());
De manera similar, puede crear un módulo para Library2.
Y luego puedes crear un módulo para Main dependiendo de estos dos:
//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());
Finalmente puedes cargar la clase principal y ejecutarla a través de la reflexión:
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]);
Puede descargar el ejemplo completo de trabajo aquí