JDK ClassLoader.getResourceAsStream坏了吗? (未公开的资源)

时间:2015-02-13 09:14:02

标签: java resources classloader java-8 resource-leak

我会尝试证明ClassLoader.getResourceAsStream()正在打开两个InputStreams,不会关闭它,只返回一个客户端。我的逻辑是否正确? JDK源是从jdk1.8.0_25

中选取的

我在区间(original question)中使用Spring ClassPathResource进入未封闭的资源问题,即使用ClassLoader.getResourceAsStreamInputStream获取到属性文件。

经过调查,我发现classLoader.getResourceAsStream正在URL获得URL url = getResource(name);,然后它会打开该流,但 URL url = getResource(name)已打开该流< / strong>即可。 JDK 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)中静默打开一个流,只是为了获取进一步用于创建**第二个的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)我们将Resource转换为URL 忘记在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);正在打开资源?让我们看一下这个方法: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()方法Resource没有关闭它InputStream只能使用URL对象打开另一个InputStream并将其返回给客户端(客户端可以关闭coruse,但我们最终结束)第一流未公开)。

那么,是ClassLaoder.getResourceAsStream是否因资源泄漏而破裂?

实用方面:我在getResourceAsStream块中使用try-with-resources,并且在生产中仍然存在未封闭的资源问题,文件名每30秒加载一次。此外,所有资源都在垃圾收集时关闭,这与close()方法中的文件流finalize()一致。

1 个答案:

答案 0 :(得分:2)

我做了一个简单的测试程序来验证实际行为:

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_051.8.0_201.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访问的所有资源也正确关闭,因此在这些情况下没有资源泄漏。

关于引导程序资源行为的代码分析是正确的,存在资源泄漏但通常不会对应用程序所需的资源发生这种情况,因为这些资源应该可以通过用户类路径访问。 ClassLoader首先尝试他们的父母,但不应在引导类路径中找到您的资源,因此该尝试应该返回null而不打开任何资源。

因此,确保通过JRE的引导类路径无法访问特定于应用程序的资源至关重要,例如:不要操纵引导类路径,也不要将资源放入JRE的扩展目录中。这也适用于上面的测试代码,如果您更改测试的顺序,即首先修补引导类路径,所有测试都将显示泄漏,因为所有查找首先尝试其父项,结束于引导加载程序。