18

アプリケーションの特定の部分について、既存のアプリケーションにプラグイン機能を追加する必要があります。実行時にjarを追加できるようにしたいのですが、アプリケーションはアプリを再起動せずにjarからクラスをロードできる必要があります。ここまでは順調ですね。URLClassLoader を使用していくつかのサンプルをオンラインで見つけましたが、正常に動作します。

また、更新されたバージョンの jar が利用可能になったときに、同じクラスをリロードする機能も必要でした。私は再びいくつかのサンプルを見つけました。私が理解しているように、これを達成するための鍵は、新しいロードごとに新しいクラスローダー インスタンスを使用する必要があるということです。

サンプル コードをいくつか書きましたが、NullPointerException が発生しました。まず、コードをお見せしましょう:

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 は、メソッド doProc を 1 つだけ持つ単純なインターフェイスです。

public interface IPlugin {
    void doProc();
}

jartest.TestPlugin は、doProc がいくつかのステートメントを出力するだけのこのインターフェースの実装です。

ここで、jartest.TestPlugin クラスを test.jar という名前の jar にパッケージ化し、C:\plugins の下に配置して、このコードを実行します。最初の反復はスムーズに実行され、クラスは問題なくロードされます。

プログラムがスリープ ステートメントを実行しているときに、C:\plugins\test.jar を同じクラスの更新されたバージョンを含む新しい jar に置き換え、while の次の反復を待ちます。今、ここで私が理解していないものがあります。更新されたクラスが問題なくリロードされる場合があります。つまり、次の反復が正常に実行されます。ただし、例外がスローされることがあります。

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)

私はネットで検索して頭をかきましたが、なぜこの例外がスローされるのか、そしてそれについても結論を出すことはできません-常にではなく、時々だけです.

これを理解するには、あなたの経験と専門知識が必要です。このコードの何が問題になっていますか? 助けてください!!

さらに情報が必要な場合はお知らせください。ご覧いただきありがとうございます。

4

6 に答える 6

16

みんなの利益のために、本当の問題と私にとってうまくいった解決策を要約させてください.

Ryan が指摘したように、JVM には Windows プラットフォームに影響するバグがあります。URLClassLoaderは、クラスをロードするために開いている jar ファイルを開いた後、開いている jar ファイルを閉じず、事実上 jar ファイルをロックします。jar ファイルは削除または置換できません。

解決策は簡単です。開いている jar ファイルを読み終わったら閉じます。ただし、開いているjarファイルへのハンドルを取得するには、リフレクションを使用する必要があります。これは、トラバースする必要があるプロパティが公開されていないためです。だから私たちはこの道をたどります

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

開いている jar ファイルを閉じるコードは、URLClassLoader を拡張するクラスの close() メソッドに追加できます。

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

(このコードは、Ryan が投稿した 2 番目のリンクから取得したものです。このコードは、バグ レポート ページにも投稿されています。)

ただし、問題があります。このコードが機能し、開いている jar ファイルへのハンドルを取得してそれらを閉じることができるようにするには、URLClassLoader 実装によってファイルからクラスをロードするために使用されるローダーがJarLoader. (メソッド) のソース コードを見ると、URL の作成に使用されるファイル文字列が "/" で終わらない場合にのみ JARLoader が使用されることに気付きました。したがって、URL は次のように定義する必要があります。URLClassPathgetLoader(URL url)

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

全体的なクラス ロード コードは次のようになります。

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

更新: JRE 7 では、この問題を解決した可能性のあるclose()メソッドがクラスに導入されました。URLClassLoader確認していません。

于 2010-12-10T07:57:00.490 に答える
11

この動作は、jvm 2のバグに関連しています。回避策は、ここに記載されています。

于 2010-07-10T04:28:42.080 に答える
1

これは、成功した Java 7 でテストされたアップデートです。今はうまくいきますURLClassLoader

マイリローダー

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

...
}

ClassReloader を閉じて再起動する

次に、jarを更新して呼び出します

classLoader.close();

その後、新しいバージョンでアプリを再起動できます。

jar を基本クラス ローダーに含めないでください。

MyReloaderMain.class.getClassLoader()" " の基本クラス ローダー " " にjar を含めないでください。つまり、" " 用の 2 つの jar と実際のアプリケーション用の 1 つを使用MyReloaderMainして 2 つのプロジェクトを開発します。MyReloaderMain誰が何をロードしているかを理解します。

于 2015-07-25T07:32:35.187 に答える