2011-08-02 24 views
26

Tôi vẫn còn tương đối mới với Java, vì vậy hãy chịu với tôi.Jar địa ngục: làm thế nào để sử dụng một bộ nạp lớp để thay thế một phiên bản thư viện jar với nhau tại thời gian chạy

Vấn đề của tôi là ứng dụng Java của tôi phụ thuộc vào hai thư viện. Hãy gọi họ là Thư viện 1 và Thư viện 2. Cả hai thư viện chia sẻ một sự phụ thuộc lẫn nhau về Thư viện 3. Tuy nhiên:

  • Thư viện 1 yêu cầu chính xác phiên bản 1 của Thư viện 3.
  • Thư viện 2 đòi hỏi chính xác phiên bản 2 của Thư viện 3.

Đây chính xác là định nghĩa của JAR hell (hoặc ít nhất một biến thể của nó). Như đã nêu trong liên kết, tôi không thể tải cả hai phiên bản của thư viện thứ ba trong cùng một trình nạp lớp. Vì vậy, tôi đã cố gắng tìm ra nếu tôi có thể tạo một trình nạp lớp mới trong ứng dụng để giải quyết vấn đề này. Tôi đã xem xét URLClassLoader, nhưng tôi đã không thể tìm ra.

Đây là cấu trúc ứng dụng ví dụ minh họa sự cố. Lớp Main (Main.java) của ứng dụng cố gắng để nhanh chóng cả library1 và Library2 và chạy một số phương pháp quy định tại các thư viện:

Main.java (phiên bản gốc, trước khi bất kỳ nỗ lực tại một giải pháp):

public class Main { 
    public static void main(String[] args) { 
     Library1 lib1 = new Library1(); 
     lib1.foo(); 

     Library2 lib2 = new Library2(); 
     lib2.bar(); 
    } 
} 

library1 và Library2 cả chia sẻ một sự phụ thuộc lẫn nhau về Library3, nhưng library1 đòi hỏi chính xác phiên bản 1, và Library2 đòi hỏi chính xác phiên bản 2. trong ví dụ này, cả hai thư viện chỉ in các phiên bản của Library3 rằng họ nhìn thấy:

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

Và sau đó, tất nhiên, có rất nhiều phiên bản của Library3. Tất cả họ làm là in số phiên bản của họ:

Phiên bản 1 của Library3 (theo yêu cầu của library1):

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

Phiên bản 2 của Library3 (theo yêu cầu của Library2):

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

Khi tôi khởi chạy ứng dụng, đường dẫn lớp chứa Library1 (lib1.jar), Library2 (lib2.jar) và phiên bản 1 của Thư viện 3 (lib3-v1/lib3.jar). Điều này làm việc tốt cho Library1, nhưng nó sẽ không hoạt động đối với Library2.

Điều tôi bằng cách nào đó cần làm là thay thế phiên bản Library3 xuất hiện trên đường dẫn lớp trước khi khởi tạo Library2. Tôi đã có ấn tượng rằng URLClassLoader có thể được sử dụng cho điều này, vì vậy, đây là những gì tôi đã thử:

Main.java (phiên bản mới, bao gồm nỗ lực của tôi tại một giải pháp):

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

Khi tôi chạy này, lib1.foo() nguyên nhân "Đây là phiên bản 1." Được in. Vì đó là phiên bản Library3 nằm trên classpath khi ứng dụng bắt đầu, điều này được mong đợi.

Tuy nhiên, tôi đã mong đợi lib2.bar() để in "Đây là phiên bản 2.", phản ánh phiên bản mới của Library3 đã được tải nhưng vẫn in "Đây là phiên bản 1."

Tại sao việc sử dụng trình nạp lớp mới có phiên bản bình phải được tải vẫn dẫn đến phiên bản jar cũ đang được sử dụng? Tôi có làm điều gì sai? Hay tôi không hiểu khái niệm đằng sau các trình nạp lớp? Làm thế nào tôi có thể chuyển đổi các phiên bản jar của Library3 một cách chính xác khi chạy?

Tôi sẽ đánh giá cao bất kỳ trợ giúp nào về vấn đề này.

+0

bản sao có thể có của http://stackoverflow.com/questions/6105124/java-classpath-classloading-multiple-versions-of-the-same-jar-project – abalogh

+4

người đã phát minh ra thuật ngữ * có thể * trùng lặp trên SO? nó có nghĩa là gì? – irreputable

+0

@svkk FYI JDK8 sẽ có [Project Jigsaw] (http://openjdk.java.net/projects/jigsaw/doc/draft-java-module-system-requirements-12) nhằm giải quyết vấn đề Jar-hell . – Bringer128

Trả lời

1

Đang cố gắng để thoát khỏi classpath lib2 và gọi phương thức bar() bởi phản ánh:

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

cho sau đầu ra:

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) 

Điều này có nghĩa bạn đang ở trong thực tế nạp Library2 từ classpath sử dụng classloader mặc định , không phải tùy chỉnh của bạn URLClassLoader.

0

classloader là một cái gì đó đơn giản trong khái niệm, nhưng thực sự khá phức tạp

tôi khuyên bạn không sử dụng một giải pháp tùy chỉnh

bạn có một số giải pháp mã nguồn mở một phần, chẳng hạn như DCEVM

nhưng có cũng là sản phẩm thương mại rất tốt, chẳng hạn như JRebel

2

Bạn cần tải cả Library1 và Library2 vào các trình tải URLClassload riêng biệt. (Trong mã hiện tại của bạn, Library2 được nạp trong một URLClassLoader mà cha mẹ là classloader chính - mà đã nạp library1.)

Thay đổi ví dụ của bạn để một cái gì đó như thế này:

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

Nó không hoạt động. Lỗi java.lang.NoClassDefFoundError xuất hiện ngay sau khi bạn thử chạy chương trình. Tôi nghĩ là @janos đề cập đến, việc khởi tạo Library3 được thực hiện bằng cách sử dụng trình nạp lớp mặc định. –

0

Sử dụng jar class loader mà có thể được sử dụng để tải các lớp từ các tệp jar trong thời gian chạy.

0

Bạn có thể sử dụng ParentLastClassloader để giải quyết Jar Hell. Vui lòng kiểm tra this blog post ra

6

Tôi không thể tin rằng trong hơn 4 năm không ai trả lời câu hỏi này một cách chính xác.

https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html

Lớp ClassLoader sử dụng một mô hình đoàn đi tìm kiếm cho các lớp và nguồn lực. Mỗi trường hợp của ClassLoader có một trình tải lớp phụ huynh được liên kết.Khi được yêu cầu tìm một lớp học hoặc tài nguyên, một cá thể của ClassLoader sẽ phân bổ tìm kiếm cho lớp hoặc tài nguyên cho trình tải lớp cấp độ gốc của nó trước khi cố gắng tự tìm lớp học hoặc tài nguyên . Trình nạp lớp được tích hợp sẵn của máy ảo, được gọi là "bộ nạp lớp bootstrap", không có bố mẹ nhưng có thể đóng vai trò là cha mẹ của một cá thể ClassLoader.

Sergei, vấn đề với ví dụ của bạn là Thư viện 1,2 & 3 là trên con đường lớp mặc định, vì vậy classloader ứng dụng đó là mẹ của URLClassloder của bạn đã có thể nạp các lớp từ Thư viện 1,2 & 3.

Nếu bạn xóa các thư viện khỏi đường dẫn lớp, Trình nạp lớp ứng dụng sẽ không thể phân giải các lớp từ chúng để nó sẽ ủy nhiệm sự phân giải cho con của nó - URLClassLoader. Vì vậy, đó là những gì bạn cần làm.

0

Tôi muốn đề xuất giải pháp sử dụng JBoss-Modules.

Bạn chỉ cần tạo một module cho 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()); 

Trong một cách tương tự bạn có thể tạo một module cho Library2.

Và sau đó bạn có thể tạo một module cho chính tùy thuộc vào hai:

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

Cuối cùng bạn có thể tải các lớp chính và chạy nó thông qua phản ánh:

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]); 

Bạn có thể tải đầy đủ ví dụ làm việc here

Các vấn đề liên quan