JDK ClassLoader.getResourceAsStream 坏了吗? (未关闭的资源)
Is JDK ClassLoader.getResourceAsStream broken? (unclosed resources)
我将尝试证明 ClassLoader.getResourceAsStream()
打开两个 InputStreams
,关闭 none 并且 return 只向客户发送一个。 我的逻辑对吗? JDK源码取自jdk1.8.0_25
我在间隔 (original question) 中使用 Spring ClassPathResource 遇到了未关闭的资源问题,即使用 ClassLoader.getResourceAsStream
将 InputStream
获取到属性文件。
经过调查,我发现 classLoader.getResourceAsStream
正在通过 URL url = getResource(name);
获取 URL
然后它正在打开该流,但是 URL url = getResource(name)
已经打开那个流。 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 对象,该对象进一步用于创建 **second(returned 到客户端)流**。看看这个方法来源:
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()
,它们没有关闭,并且通过所有调用 return 回退 Resource
我们最终只使用 getURL()
方法包装在 Resource
中而不关闭它 InputStream
只是为了使用那个 URL
对象打开另一个 InputStream
和 return 它给客户端(哪个客户端可以关闭 coruse,但我们以第一个流未关闭结束)。
那么,是否因资源泄漏而 ClassLaoder.getResourceAsStream 损坏?
实际方面:我在 try-with-resources
块中使用 getResourceAsStream
,并且在生产中仍然存在未关闭的资源问题,文件名每 30 秒以这种方式加载一次。此外,所有资源在垃圾回收时都关闭,这与 finalize()
方法中的文件流 close()
一致。
我做了一个简单的测试程序来验证实际行为:
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
所以从这个测试中,我可以得出结论,资源确实打开了两次,但对于通过用户 class 路径和其他 ClassLoader
s 访问的所有资源也正确关闭了,所以没有资源泄漏在这些情况下。
您对 bootstrap 资源行为的代码分析是正确的,存在资源泄漏,但对于您的应用程序所需的资源通常不会发生这种情况,因为这些应该可以通过 user 访问class路径。 ClassLoader
s 首先尝试他们的 parents 但你的资源不应该在 bootstrap class 路径 中找到,因此尝试应该 return null
而不是打开任何资源。
因此,确保无法通过 JRE 的 bootstrap class 路径访问特定于应用程序的资源至关重要,例如不要操纵 bootstrap class 路径,也不要将资源放入 JRE 的扩展目录中。这也适用于上面的测试代码,如果您更改测试的顺序,即首先修补 bootstrap class 路径,所有测试都将显示泄漏,因为所有查找都会尝试其 parent首先,结束于 bootstrap 加载程序。
我将尝试证明 ClassLoader.getResourceAsStream()
打开两个 InputStreams
,关闭 none 并且 return 只向客户发送一个。 我的逻辑对吗? JDK源码取自jdk1.8.0_25
我在间隔 (original question) 中使用 Spring ClassPathResource 遇到了未关闭的资源问题,即使用 ClassLoader.getResourceAsStream
将 InputStream
获取到属性文件。
经过调查,我发现 classLoader.getResourceAsStream
正在通过 URL url = getResource(name);
获取 URL
然后它正在打开该流,但是 URL url = getResource(name)
已经打开那个流。 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 对象,该对象进一步用于创建 **second(returned 到客户端)流**。看看这个方法来源:
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()
,它们没有关闭,并且通过所有调用 return 回退 Resource
我们最终只使用 getURL()
方法包装在 Resource
中而不关闭它 InputStream
只是为了使用那个 URL
对象打开另一个 InputStream
和 return 它给客户端(哪个客户端可以关闭 coruse,但我们以第一个流未关闭结束)。
那么,是否因资源泄漏而 ClassLaoder.getResourceAsStream 损坏?
实际方面:我在 try-with-resources
块中使用 getResourceAsStream
,并且在生产中仍然存在未关闭的资源问题,文件名每 30 秒以这种方式加载一次。此外,所有资源在垃圾回收时都关闭,这与 finalize()
方法中的文件流 close()
一致。
我做了一个简单的测试程序来验证实际行为:
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
所以从这个测试中,我可以得出结论,资源确实打开了两次,但对于通过用户 class 路径和其他 ClassLoader
s 访问的所有资源也正确关闭了,所以没有资源泄漏在这些情况下。
您对 bootstrap 资源行为的代码分析是正确的,存在资源泄漏,但对于您的应用程序所需的资源通常不会发生这种情况,因为这些应该可以通过 user 访问class路径。 ClassLoader
s 首先尝试他们的 parents 但你的资源不应该在 bootstrap class 路径 中找到,因此尝试应该 return null
而不是打开任何资源。
因此,确保无法通过 JRE 的 bootstrap class 路径访问特定于应用程序的资源至关重要,例如不要操纵 bootstrap class 路径,也不要将资源放入 JRE 的扩展目录中。这也适用于上面的测试代码,如果您更改测试的顺序,即首先修补 bootstrap class 路径,所有测试都将显示泄漏,因为所有查找都会尝试其 parent首先,结束于 bootstrap 加载程序。