5

ClassLoader.getResourceAsStream()私はそれが2つを開きInputStreams、どれも閉じず、1つだけをクライアントに返すことを証明しようとします。私の論理は正しいですか?JDK ソースは jdk1.8.0_25 から選択されます

Spring ClassPathResource を間隔で使用して (元の質問)、プロパティ ファイルにアクセスClassLoader.getResourceAsStreamするために使用している、閉じられていないリソースの問題に遭遇しました。InputStream

調査の結果、それが通り抜けて、そのストリームを開いていることclassLoader.getResourceAsStreamがわかりましたURLが、すでにそのストリームを開いています。のJDKソース:URL url = getResource(name);URL url = getResource(name)ClassLoader

    public InputStream getResourceAsStream(String name) {
        URL url = getResource(name); /* SILENTLY OPENS AND DON'T CLOSES STREAM */
        try {
            return url != null ? url.openStream() : null; /* SECOND OPEN !!! */
        } catch (IOException e) {
            return null;
        }
    }

そのように指定するとclose()InputStreamによって開かれたストリームのみが閉じられurl.openStream()ます。JDK ソース:

    public final InputStream openStream() throws java.io.IOException {
        return openConnection().getInputStream();
    }

問題は、JDK がストリームを静かに開き、URL url = getResource(name) ** 2 番目の(クライアントに返される) ストリーム** を作成するためにさらに使用される URL オブジェクトを取得することです。このメソッドのソースを見てください:

    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name); <---- we end up calling that method
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }

そして今、開いたストリームを忘れるgetBootstrapResource(name)瞬間に:ResourceURL Resource

private static URL getBootstrapResource(String name) {
    URLClassPath ucp = getBootstrapClassPath();
    Resource res = ucp.getResource(name); <---- OPENING STREAM [see further]
    return res != null ? res.getURL() : null; <--- LOSING close() CAPABILITY
}

ucp.getResource(name);リソースを開くのはなぜですか?その method: を見てみましょう。これは次のようにthis.getResource(var1, true);委譲します:

public Resource getResource(String var1, boolean var2) {
    if(DEBUG) {
        System.err.println("URLClassPath.getResource(\"" + var1 + "\")");
    }

    URLClassPath.Loader var3;
    for(int var4 = 0; (var3 = this.getLoader(var4)) != null; ++var4) {
        Resource var5 = var3.getResource(var1, var2); <-------- OPENING STREAM
        if(var5 != null) {
            return var5;
        }
    }

    return null;
}

Resource var5 = var3.getResource(var1, var2);ストリームを開くのはなぜですか?さらに見てください:

Resource getResource(final String var1, boolean var2) {
        final URL var3;
        try {
            var3 = new URL(this.base, ParseUtil.encodePath(var1, false));
        } catch (MalformedURLException var7) {
            throw new IllegalArgumentException("name");
        }

        final URLConnection var4;
        try {
            if(var2) {
                URLClassPath.check(var3);
            }

            var4 = var3.openConnection(); <------------ OPENING STREAM
            InputStream var5 = var4.getInputStream();
            if(var4 instanceof JarURLConnection) {
                JarURLConnection var6 = (JarURLConnection)var4;
                this.jarfile = URLClassPath.JarLoader.checkJar(var6.getJarFile());
            }
        } catch (Exception var8) {
            return null;
        }

        return new Resource() {
            public String getName() {
                return var1;
            }

            public URL getURL() {
                return var3;
            }

            public URL getCodeSourceURL() {
                return Loader.this.base;
            }

            public InputStream getInputStream() throws IOException {
                return var4.getInputStream();
            }

            public int getContentLength() throws IOException {
                return var4.getContentLength();
            }
        };
    }

と が閉じられていないことを確認できopenConnection()getInputStream()すべての呼び出しが戻っResourceてきてフォールバックすることがわかります。最終的には、閉じずにgetURL()ラップされたメソッドのみを使用しています。そのオブジェクトを使用して別のジェットを開き、それをクライアントに返すだけですただし、最初のストリームが閉じられていない状態で終了します)。ResourceInputStreamURLInputStream

では、ClassLaoder.getResourceAsStream はリソースのリークで壊れていますか?

実用的な側面: 私はブロックで使用getResourceAsStreamしてtry-with-resourcesいますが、まだ 30 秒ごとにファイル名がロードされている実稼働環境で、クローズされていないリソースの問題があります。close()さらに、すべてのリソースはガベージ コレクションで閉じられます。これはファイル ストリームインfinalize()メソッドと一致しています。

4

1 に答える 1

3

実際の動作を確認するための簡単なテスト プログラムを作成しました。

System.out.println(System.getProperty("java.version"));
URL testURL = new URL("test", null, 0, "/", new URLStreamHandler() {
    protected URLConnection openConnection(URL u) throws IOException {
        System.out.println("creating connection to "+u);
        return new URLConnection(u) {
            InputStream is;
            public void connect(){}
            @Override
            public InputStream getInputStream() throws IOException {
                System.out.println("getInputStream() for "+u);
                if(is==null) is=new InputStream() {
                    boolean open=true;
                    @Override
                    public void close() throws IOException {
                        if(!open) return;
                        System.out.println("One InputStream for "+u+" closed");
                        open=false;
                    }
                    public int read() { return -1; }
                };
                else System.out.println("COULD be shared");
                return is;
            }
        };
    }
});
System.out.println("\n  trying new ClassLoader");
try(URLClassLoader newlClassLoader=new URLClassLoader(new URL[]{ testURL });
    InputStream is=newlClassLoader.getResourceAsStream("foo")) {}

System.out.println("\n  trying System ClassLoader");
try {
    Method m=URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    m.setAccessible(true);
    m.invoke(ClassLoader.getSystemClassLoader(), testURL);
} catch(Exception ex) { ex.printStackTrace(); }
try(InputStream is=ClassLoader.getSystemResourceAsStream("foo")) {}

System.out.println("\n  trying bootstrap ClassLoader");
try {
    Method m=ClassLoader.class.getDeclaredMethod("getBootstrapClassPath");
    m.setAccessible(true);
    Object bootstrap = m.invoke(null);
    m=bootstrap.getClass().getDeclaredMethod("addURL", URL.class);
    m.setAccessible(true);
    m.invoke(bootstrap, testURL);
} catch(Exception ex) { ex.printStackTrace(); }

try(InputStream is=ClassLoader.getSystemClassLoader().getResourceAsStream("foo")) {}

1.8.0_05( 、1.8.0_20およびでテストされた)を使用して私のマシン1.8.0_40で印刷されました

  trying new ClassLoader
creating connection to test:/foo
getInputStream() for test:/foo
One InputStream for test:/foo closed
creating connection to test:/foo
getInputStream() for test:/foo
One InputStream for test:/foo closed

  trying System ClassLoader
creating connection to test:/foo
getInputStream() for test:/foo
One InputStream for test:/foo closed
creating connection to test:/foo
getInputStream() for test:/foo
One InputStream for test:/foo closed

  trying bootstrap ClassLoader
creating connection to test:/foo
getInputStream() for test:/foo
creating connection to test:/foo
getInputStream() for test:/foo
One InputStream for test:/foo closed

ClassLoaderしたがって、このテストから、リソースは実際に 2 回開かれているだけでなく、ユーザー クラス パスと追加の を介してアクセスされるすべてのリソースに対して正しく閉じられているため、これらの場合にリソース リークはないと結論付けることができます。

ブートストラップ リソースの動作に関するコード分析は正しいです。リソース リークがありますが、アプリケーションで必要なリソースはユーザー クラス パス経由でアクセスできるため、通常、これは発生しません。ClassLoader最初に親を試しますが、リソースがブートストラップ クラス パスで見つからないはずnullです。

したがって、JRE のブートストラップ クラス パスを介してアプリケーション固有のリソースにアクセスできないようにすることが重要です。たとえば、ブートストラップ クラス パスを操作したり、リソースを JRE の拡張ディレクトリに配置したりしないでください。これは上記のテスト コードにも当てはまります。テストの順序を変更した場合、つまり最初にブートストラップ クラス パスにパッチを適用すると、すべてのルックアップが最初に親を試行し、ブートストラップ ローダーで終了するため、すべてのテストでリークが表示されます。

于 2015-02-13T10:25:19.780 に答える