web-dev-qa-db-ja.com

Jar hell:クラスローダーを使用して実行時に1つのjarライブラリバージョンを別のバージョンに置き換える方法

私はまだJavaに比較的慣れていないので、ご容赦ください。

私の問題は、私のJavaアプリケーションが2つのライブラリに依存していることです。それらをライブラリ1とライブラリ2と呼びましょう。これらのライブラリは両方ともライブラリ3に相互依存しています。ただし:

  • ライブラリ1には、ライブラリ3のバージョン1が必要です。
  • ライブラリ2には、ライブラリ3のバージョン2が必要です。

これはまさに JAR hell (または少なくとも1つのバリエーション)の定義です。リンクに記載されているように、3番目のライブラリの両方のバージョンを同じクラスローダーにロードすることはできません。したがって、この問題を解決するために、アプリケーション内に新しいクラスローダーを作成できるかどうかを調べようとしています。 RLClassLoader を調べていましたが、理解できませんでした。

これは、問題を示すアプリケーション構造の例です。アプリケーションのMainクラス(Main.Java)は、Library1とLibrary2の両方をインスタンス化し、それらのライブラリで定義されているメソッドを実行しようとします。

Main.Java(元のバージョン、解決を試みる前):

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

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

Library1とLibrary2はどちらもLibrary3への相互依存関係を共有していますが、Library1には正確にバージョン1が必要であり、Library2には正確にバージョン2が必要です。この例では、これらのライブラリは両方とも、表示されるLibrary3のバージョンを出力するだけです。

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

そしてもちろん、Library3には複数のバージョンがあります。彼らがすることは、バージョン番号を印刷することだけです。

Library3のバージョン1(Library1に必要):

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

Library3のバージョン2(Library2に必要):

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

アプリケーションを起動すると、クラスパスにはLibrary1(lib1.jar)、Library2(lib2.jar)、およびLibrary 3のバージョン1(lib3-v1/lib3.jar)が含まれています。これはLibrary1では正常に機能しますが、Library2では機能しません。

Library2をインスタンス化する前に、クラスパスに表示されるLibrary3のバージョンを置き換える必要があります。 RLClassLoader を使用できるという印象を受けたので、これを試しました。

Main.Java(新しいバージョン、解決策の試みを含む):

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

これを実行すると、lib1.foo()によって「これはバージョン1です」と表示されます。印刷する。これは、アプリケーションの起動時にクラスパスにあるLibrary3のバージョンであるため、これは予想されます。

ただし、新しいバージョンのLibrary3が読み込まれたことを反映して、lib2.bar()が「これはバージョン2です」と出力することを期待していましたが、「これはバージョン1です」と出力されます。

正しいjarバージョンがロードされた状態で新しいクラスローダーを使用しても、古いjarバージョンが使用されるのはなぜですか?私は何か間違ったことをしていますか?または、クラスローダーの背後にある概念を理解していませんか? Library3のjarバージョンを実行時に正しく切り替えるにはどうすればよいですか?

この問題について助けていただければ幸いです。

32
Sergey K

4年以上この質問に正しく答えた人がいないとは信じられません。

https://docs.Oracle.com/javase/8/docs/api/Java/lang/ClassLoader.html

ClassLoaderクラスは、委任モデルを使用してクラスとリソースを検索します。 ClassLoaderの各インスタンスには、関連する親クラスローダーがあります。クラスまたはリソースの検索が要求されると、ClassLoaderインスタンスは、クラスまたはリソース自体を検索する前に、クラスまたはリソースの検索をその親クラスローダーに委任します。 「ブートストラップクラスローダー」と呼ばれる仮想マシンの組み込みクラスローダーには、それ自体には親がありませんが、ClassLoaderインスタンスの親として機能する場合があります。

セルゲイ、あなたの例の問題は、ライブラリ1、2、3がデフォルトのクラスパスにあるため、URLClassloderの親であるアプリケーションクラスローダーがライブラリ1、2、3からクラスをロードできたことです。

クラスパスからライブラリを削除すると、アプリケーションクラスローダーはライブラリからクラスを解決できないため、解決をその子であるURLClassLoaderに委任します。だからそれはあなたがする必要があることです。

8
mdzh

Library1とLibrary2の両方を別々のURLClassloaderにロードする必要があります。 (現在のコードでは、Library2は、親がメインのクラスローダーであるURLClassloaderにロードされています。これはすでにLibrary1をロードしています。)

例を次のように変更します。

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();
2
Dave DiFranco

_classpath lib2_を取り除き、リフレクションによってbar()メソッドを呼び出そうとしています。

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

次の出力が得られます。

_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)
_

これは、実際には、カスタムclasspathではなく、デフォルトのクラスローダーを使用してURLClassLoaderから_Library2_をロードしていることを意味します。

1
Vlad

クラスローダーは概念的には単純なものですが、実際には非常に複雑です

カスタムソリューションを使用しないことをお勧めします

[〜#〜] dcevm [〜#〜] などの部分的なオープンソースソリューションがあります

しかし、 JRebel などの非常に優れた商用製品もあります。

0
wikier

JBoss-Modulesを使用した解決策を提案します。

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

同様の方法で、Library2のモジュールを作成できます。

そして、次の2つに応じて、Main用のモジュールを作成できます。

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

最後に、Mainクラスをロードして、リフレクションを実行できます。

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

完全な実例をダウンロードできます ここ

0
fonkap

ParentLastClassloaderを使用して、JarHellを解決できます。チェックしてください このブログ投稿 アウト

0
Jeewantha

jarクラスローダー を使用します。これは、実行時にjarファイルからクラスをロードするために使用できます。

0
Kamran