这是静态代码扫描系列文章的第五篇,前四篇回顾:
- 静态代码扫描 (一)——PMD 自定义规则入门
- 静态代码扫描 (二)——PMD 自定义规则实践
- 静态代码扫描 (三)——FindBugs 自定义规则入门
- 静态代码扫描 (四)——Java 资源关闭研究
在上一篇文章中,我主要介绍了为什么要手动关闭 Java 资源对象和怎样正确的手动关闭 Java 资源对象。这篇文章将继续分享在判断 Java 资源关闭时,有哪些特殊的场景。
通常我们判断一个资源对象是否被关闭,只需要关注该资源对象在使用后有没有调用 close() 方法,如果考虑的更严谨一些,还需要判断是否在 finally 中关闭或者使用 try-with-resources 写法。但经过我们的业务实践,发现还有更多的场景需要考虑:
- 开发使用自定义类进行资源对象关闭,例如 QHRecyleUtils.safeClose(xxx),我们如何应对?
- 开发封装了一个方法,该方法最后 return 该资源对象,这种情况怎么处理?
- 有些资源对象是不需要检查是否关闭的,有哪些?为什么?
- 当一个方法内的多个资源对象初始化的过程中是有关联的,检查关闭的机制要有相应的变化,为什么?
对于以上这些问题,下文中将一一解答。
在使用火线扫描本公司的项目代码时,发现几乎所有的项目都会使用自定义的类来统一管理资源关闭。例如:
/**
* 资源回收工具类
*/
public final class QHRecyleUtils {
/**
* 回收InputStream类型
* @param inputStream 要回收的资源
*/
public static void close(InputStream inputStream) {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
}
}
}
/**
* 回收OutputStream类型
* @param outputStream 要回收的资源
*/
public static void close(OutputStream outputStream) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
}
}
}
//还有Reader、Writer等等,这里就不一一列举了。
}
有个这个统一的工具类,开发在关闭资源对象时,只需要调用 QHRecyleUtils.close(e)
即可。例如:
public void test_01(){
FileOutputStream fos;
OutputStreamWriter osw=null;
try {
fos = new FileOutputStream("e:/a.txt");
osw = new OutputStreamWriter(fos,"UTF-8");
osw.append("55555");
} catch (Exception e) {
// TODO: handle exception
}finally{
QHRecyleUtils.close(osw);//这里调用了工具类进行关闭。
}
}
当静态代码分析遇到这种场景时,就需要追踪进入 QHRecyleUtils 类中的 close() 方法,确认该资源对象是否真的执行了关闭操作。
存在一种这样的写法,把针对资源对象做的操作封装起来,方法最终返回的是操作完成后的资源对象。这种情况下,是不需要关闭该资源对象的。例如:
private static InputStream getNewInputStream() throws IOException {
InputStream in = null;
try {
in = new URL("http://www.so.com").openStream();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return in;
}
这段代码表明开发是想通过封装一个方法获取该资源对象,所以在该方法内,是不需要关闭该资源对象的。
有一些资源对象时不用关闭的。这些对象包括:ByteArrayInputStream、ByteArrayOutputStream、StringBufferInputStream、CharArrayWriter、和 StringWriter。
看官方文档里面对象类对应的 close() 方法的解释:
Closing a ByteArrayInputStream has no effect. The methods in this class can be called after the stream has been closed without generating an IOException.
关闭 ByteArrayInputStream 对象没有效果。即使资源对象已经关闭了,再调用这个关闭方法也不会生成 IOException。
既然关闭方法没有关闭的效果,那么在静态代码检测时就没有必要检查该资源对象是否调用了 close() 方法。
更多详情请见官方文档。
先来看一段代码来理解资源对象的套接是什么:
FileOutputStream fileOutputStream = new FileOutputStream("A.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
DataOutputStream out = new DataOutputStream(bufferedOutputStream);
以上代码中,首先定义了 fileOutputStream,选择 A.txt 作为输出文件。
接着第二行 bufferedOutputStream 套接了 fileOutputStream,为输出提供缓冲功能。
第三行 out 套接了 bufferedOutputStream,提供基本数据类型的写入功能。
在两次套接后,我们最后只需要关闭最后套接的 out 对象,即调用out.close()
,之前被套接的两个对象也会自动被关闭。
为什么被套接的两个对象也会自动被关闭?
重点在于 close() 方法的实现,out 在关闭时实际是先调用了 java.io.FilterOutputStream.close() 方法,该方法的具体实现如下:
/**
* Closes this output stream and releases any system resources
* associated with the stream.
* <p>
* The <code>close</code> method of <code>FilterOutputStream</code>
* calls its <code>flush</code> method, and then calls the
* <code>close</code> method of its underlying output stream.
*
* @exception IOException if an I/O error occurs.
* @see java.io.FilterOutputStream#flush()
* @see java.io.FilterOutputStream#out
*/
public void close() throws IOException {
try {
flush();
} catch (IOException ignored) {
}
out.close();
}
这段代码意思是先调用了 flush() 方法,保证之前写入到内存的数据刷到硬盘。接着调用 java.io.OutputStream.close() 方法,继续看 java.io.OutputStream.close() 的实现:
/**
* Closes this output stream and releases any system resources
* associated with this stream. The general contract of <code>close</code>
* is that it closes the output stream. A closed stream cannot perform
* output operations and cannot be reopened.
* <p>
* The <code>close</code> method of <code>OutputStream</code> does nothing.
*
* @exception IOException if an I/O error occurs.
*/
public void close() throws IOException {
}
注释中Closes this output stream and releases any system resources associated with this stream
这句非常重要,意思是关闭这个输出流并释放任何与之相关的系统资源。
大家还可以看到方法里面什么都没有做,但是 java.io.OutputStream 实现了 Closeable 接口,接着 Closeable 接口集成了 AutoCloseable 接口,最后定位到 AutoCloseable 接口中,注释里有这样一句Closes this resource, relinquishing any underlying resources.
,大意为关闭这个资源,放弃任何底层的资源。有兴趣深入研究的同学,另附官方文档
这个逻辑跟第二点套接流对象关闭的逻辑正好相反,我们结合代码来看一下。
Statement stmt = con.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT a, b FROM TABLE2");
从代码可以看出 Statement 和 ResultSet 的关系,其中当 Statement 对象关闭之后,由 Statement 对象初始化的 ResultSet 对象 rs 也会被自动关闭。
When a Statement object is closed, its current ResultSet object, if one exists, is also closed.
详情可查看官方文档
使用 socket 创建出的 InputStream 和 OutputStream,当 socket 关闭时,这两个流也会自动关闭。
同时,如果关闭 InputStream,将会同时关闭与之相关的 Socket。
Socket socket = new Socket("127.0.0.1", 8001);
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
Closing this socket will also close the socket's InputStream and OutputStream.
If this socket has an associated channel then the channel is closed as well.
Closing the returned InputStream will close the associated socket.
详情可查看官方文档
从以上 6 种特殊场景来看,想要判断一个资源对象是不是真正的关闭,还是非常复杂的。但是经过我们火线团队的努力,火线目前已经能够覆盖绝大部分场景,误报率和检出率均优于业界开源产品。我们将在下一篇文章中列出火线和其他开源产品横向的扫描结果对比报告,包括 Sonar、Lint、PMD、Findbugs 等,敬请期待。
关注公众号,第一时间收到我们推送的新文章~