2011-08-02 9 views
26

Todavía soy relativamente nuevo en Java, así que por favor tengan paciencia conmigo.Jar hell: cómo usar un cargador de clases para reemplazar una versión de la biblioteca jar con otra en el tiempo de ejecución

Mi problema es que mi aplicación Java depende de dos bibliotecas. Vamos a llamar a la biblioteca 1 y biblioteca 2. Ambas bibliotecas comparten una dependencia mutua en la Biblioteca 3. Sin embargo:

  • Biblioteca 1 requiere exactamente la versión 1 de la Biblioteca 3.
  • Biblioteca 2 requiere exactamente la versión 2 de la Biblioteca 3.

Esta es exactamente la definición de JAR hell (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 una instancia tanto Biblioteca1 y Biblioteca2 y ejecutar 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(); 
    } 
} 

Biblioteca1 y Biblioteca2 ambos comparten una dependencia mutua en Library3, pero Biblioteca1 requiere exactamente la versión 1, y Biblioteca2 requiere exactamente la versión 2. en el ejemplo, estas dos bibliotecas simplemente imprimir la versión de Library3 que ven:

Lib rary1.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 varias versiones de Library3. Lo único que hacen es imprimir sus números de versión:

Versión 1 de Library3 (requerido por Biblioteca1):

public class Library3 { 
    public void printVersion() { 
    System.out.println("This is version 1."); 
    } 
} 

versión 2 de Library3 (requerido por Biblioteca2):

public class Library3 { 
    public void printVersion() { 
    System.out.println("This is version 2."); 
    } 
} 

Cuando ejecuto 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 URLClassLoader podría usarse para esto, así que esto es lo que probé:

Principal.java (nueva versión, incluyendo 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() causas "Esta es la versión 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 todavía imprime "Esta es la versión 1."

¿Por qué es que usar el nuevo cargador de clases con la versión de jar correcta cargada todavía da como resultado la versión anterior de jar que se utiliza? ¿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?

Agradeceria cualquier ayuda sobre este problema.

+0

posible duplicado de http://stackoverflow.com/questions/6105124/java-classpath-classloading-multiple-versions-of-the-same-jar-project – abalogh

+4

que inventó el término * posible * duplicar en SO? ¿Qué significa eso? – irreputable

+0

@svkk FYI JDK8 tendrá [Project Jigsaw] (http://openjdk.java.net/projects/jigsaw/doc/draft-java-module-system-requirements-12) con la intención de resolver el problema Jar-hell . – Bringer128

Respuesta

1

Tratando de deshacerse de classpath lib2 e invocar el método bar() por la reflexión:

try { 
    cls.getMethod("bar").invoke(cls.newInstance()); 
} catch (Exception e) { 
    e.printStackTrace(); 
} 

da salida siguiente:

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 es, de hecho, la carga de Library2classpath usando cargador de clases por defecto , no es tu costumbre URLClassLoader.

0

cargador de clases son algo simple en concepto, pero en realidad bastante compleja

recomiendo que no use una solución personalizada

usted tiene algunas soluciones de código abierto parciales, tales como DCEVM

pero hay también producto comercial muy bueno, como JRebel

2

Debe cargar tanto Library1 como Library2 en URLClassloaders por separado. (En su código actual, Biblioteca2 se carga en un URLClassLoader cuyo padre es el principal cargador de clase - que ya ha cargado Biblioteca1.)

Cambiar tu 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(); 
+0

No funciona. El error java.lang.NoClassDefFoundError aparece tan pronto como intenta ejecutar el programa. Creo que como menciona @janos, la creación de instancias de Library3 se realiza mediante el cargador de clases predeterminado. –

0

Uso jar class loader que puede ser utilizado para cargar clases de archivos jar en tiempo de ejecución.

6

No puedo creer que por 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 utiliza 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 el recurso en su cargador de clases principal antes de intentar encontrar la clase o el recurso . 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 & 3 estaban en la ruta de clase por defecto, por lo que el cargador de clases de aplicaciones que era el padre de su URLClassloder fue capaz de cargar las clases de la biblioteca 1,2 & 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.

0

Sugeriría una solución usando JBoss-Modules.

Sólo es necesario para crear un módulo para Biblioteca1:

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 la misma manera se puede crear un módulo para Biblioteca2.

Y a continuación, puede crear un módulo para la principal función 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, puede cargar la clase principal y ejecutarlo 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 pleno ejemplo de trabajo here

Cuestiones relacionadas