【Exception】如何使用 try-with-resources 优雅地关闭资源

一、背景

        在 Java 编程中,如果打开了外部资源,如:文件输入输出流、数据库连接、网络连接(InputStream、OutputStream、java.sql.Connection)等,那我们必须在这些外部资源使用完后,去手动地关闭它们。因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制。如果我们在编程时并没有确保在正确的时机关闭外部资源,就会导致外部资源泄露,就会出现文件被异常占用,数据库连接过多导致连接池溢出等诸多严重的性能问题。

而关闭的方法有很多种。比如:finalizer、try-catch-finally、try-with-resources 等等。

finalizer 机制可以关闭,但是其执行性不可预测,还有可能造成内存泄漏,所以一般不使用。因此,选择就落在了 try-catch-finally 和 try-with-resources 之间。


二、使用 try-catch-finally 关闭资源

为了确保外部资源一定要被关闭,通常关闭代码被写入 finally 代码块中。当然,我们还必须注意到关闭资源时可能抛出的异常,于是便有了下面的代码:

public class FileTestTwo {

    public static void main(String[] args) {
        FileInputStream in = null;
        int length = 0;

        try {
            in = new FileInputStream("test.txt");

            byte[] bytes = new byte[1024];
            while ((length = in.read(bytes)) != -1) {
                System.out.println(new String(bytes, 0, length));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述代码:通过文件输入流读取 test.txt 文件中的内容,一次读取 1024B,每读取一次,就将读取到的内容打印到屏幕中去,最后在 finally 语句中关闭流。

再在原有的代码上加一个文件输出流。如下:

public class FileTestTwo {

    public static void main(String[] args) {
        FileInputStream in = null;
        FileOutputStream out = null;
        int length = 0;

        try {
            in = new FileInputStream("test.txt");
            out = new FileOutputStream("test2.txt");

            byte[] bytes = new byte[1024];
            while ((length = in.read(bytes)) != -1) {
                out.write(bytes, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {  // 在这里使用 finally,防止 in.close() 方法抛出异常,而没有执行 out.close() 
                    if (null != out) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

从上面代码来看,打开的资源越多,需要关闭资源的代码就越多,甚至会超过业务代码。因为我们不仅需要关闭 FileInputStream,还需要保证在执行 in.close();代码时出现了异常,FileOutputStream也能正确地被关闭。所以,不得不嵌套 finally 语句。

但值得注意的是:打开的资源越多,finally 语句就嵌套得越深!!


三、使用 try-with-resources 关闭资源

在 JDK1.7 中,新增了 try-with-resources 语法,才实现了自动关闭外部资源的功能。
使用它时,必须要求外部资源的句柄对象(如:FileInputStream 对象)实现了 AutoCloseable 接口。

那么,如何使用它呢?

将外部资源的句柄对象的创建放在 try 后面的括号中,当这个 try-catch 代码块执行完毕后,Java 会确保外部资源的close() 方法被调用。

将上述代码使用 try-with-resources 语法进行改造,代码如下:

public class FileThree {

    public static void main(String[] args) {
        try (FileInputStream in = new FileInputStream("test.txt");
             FileOutputStream out = new FileOutputStream("test2.txt")) {
            int length = 0;
            byte[] bytes = new byte[1024];

            while ((length = in.read(bytes)) != -1) {
                out.write(bytes, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这样看上去,是不是感觉代码干净了许多。当程序运行完离开try语句块时,( ) 里的资源就会被自动关闭。

但是 try-with-resources 这几个关键点要记住:

  1. try() 里面的类,必须实现了 AutoCloseable 接口
  2. 在 try() 代码中声明的资源被隐式声明为 final
  3. 使用分号分隔,可以声明多个资源

手动自定义

自己新建一个类,配合使用 try-with-resources 语法。

那么该类需要:实现 AutoClosable 接口。该接口的实现类需要重写 close() 方法。代码如下:

public class TestAutoClosable implements AutoCloseable {

    public void test() {
        System.out.println("test() 方法被调用");
    }

    @Override
    public void close() throws Exception {
        System.out.println("close() 方法被调用");
    }

    public static void main(String[] args) {
        try (TestAutoClosable test = new TestAutoClosable()) {
            test.test();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行上述代码:

test() 方法被调用
close() 方法被调用

发现 TestAutoClosable#close() 方法被调用了(代码中是没有显示调用这个 close() 方法的哈)。

那么,这个是怎么做到的呢?

这个就得看看 try-with-resources 的实现原理了

实现原理

try-with-resources 并不是 JVM 的新增功能,只是 JDK 实现了一个语法糖。当你将上面代码反编译后会发现,其实对JVM 而言,它看到的依然是之前的写法:

public class TestAutoClosable implements AutoCloseable {
    public TestAutoClosable() {
    }

    public void test() {
        System.out.println("test() 方法被调用");
    }

    public void close() throws Exception {
        System.out.println("close() 方法被调用");
    }

    public static void main(String[] args) {
        try {
            TestAutoClosable test = new TestAutoClosable();
            Throwable var2 = null;

            try {
                test.test();
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (test != null) {
                    if (var2 != null) {
                        try {
                            test.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        test.close();
                    }
                }

            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }

    }
}

从反编译后的代码来看,编译器自动帮我们生成了 finally 块,并且在里面调用了资源的 close() 方法,所以例子中的close() 方法会在运行的时候被执行。

异常屏蔽

从反编译的代码中看到了一个 addSuppressed() 方法,那么,这个方法的作用是什么呢?

为了搞清楚它,我们修改下上面的例子,如下代码:

public class TestAutoClosable implements AutoCloseable {

    public void test() throws Exception{
        throw new Exception("test() 方法被调用");
    }

    @Override
    public void close() throws Exception {
        throw new MyException("close() 方法被调用");
    }

    public static void testClose() throws Exception{
        TestAutoClosable test = null;
        try {
            test = new TestAutoClosable();
            test.test();
        } finally {
            if (null != test) {
                test.close();
            }
        }
    }
	
	// 测试
	public static void main(String[] args) {
        try {
            testClose();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

最后,控制台打印出:

java.lang.Exception: close() 方法被调用
	at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
	at com.tiandy.zzc.design.file.TestAutoClosable.testClose(TestAutoClosable.java:45)
	at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:31)

问题来了,由于我们一次只能抛出一个异常,所以在最上层看到的是最后一个抛出的异常——也就是 close() 方法抛出的 Exception ,而 抛出的 test() 方法的Exception 被忽略了。这就是所谓的异常屏蔽

由于异常信息的丢失,异常屏蔽可能会导致某些bug变得极其难以发现。为了解决这个问题,从 Java 1.7 开始,大佬们为 Throwable 类新增了 addSuppressed() 方法,支持将一个异常附加到另一个异常身上,从而避免异常屏蔽。

那么被屏蔽的异常信息会通过怎样的格式输出呢?

我们再运行一遍刚才用 try-with-resource 包裹的 main() 方法:

public static void main(String[] args) {
    try (TestAutoClosable test = new TestAutoClosable()) {
        test.testClose();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

控制台打印出:

java.lang.Exception: close() 方法被调用
	at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
	at com.tiandy.zzc.design.file.TestAutoClosable.testClose(TestAutoClosable.java:45)
	at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:25)
	Suppressed: java.lang.Exception: close() 方法被调用
		at com.tiandy.zzc.design.file.TestAutoClosable.close(TestAutoClosable.java:20)
		at com.tiandy.zzc.design.file.TestAutoClosable.main(TestAutoClosable.java:26)

可以看到,异常信息中多了一个 Suppressed 的提示,告诉我们这个异常其实由两个异常组成,close() 方法的 Exception 是被 Suppressed 的异常。


总结

处理必须关闭的资源时,始终要优先考虑使用 try-with-resources,而不是 try-finally。这样得到的代码将更简洁,清晰,产生的异常也更有价值,这些也是 try-finally 无法做到的。

上一篇:plotly基于dataframe数据绘制股票K线图并添加技术指标


下一篇:P13 preparedstatement实现表数据的增删改操作