你的分享就是我们的动力 ---﹥

Java SE 7带来更好的资源管理:不仅仅是语法糖

时间:2014-12-25 18:39来源:www.chengxuyuans.com 点击:

前言

典型的Java应用会操纵几种类型的资源,比如说文件、流(stream)、套接口和数据连接。这类资源必须要很小心地进行处理,因为它们获取系统资源来进行操作。因此,你需要确保即使是在错误发生的情况下它们也能被释放。不正确的资源管理是生产应用中一个常见的故障来源,常见的错误陷阱是,代码中的另一处有异常发生后,数据连接和文件描述符仍然保持着打开的状态。这导致了应用服务器频繁地在资源耗尽的情况出现时重新启动,因为操作系统和服务器应用通常都会有一个资源上限的限制。

Java中的资源和异常管理的正确做法已有很好的文档说明。对于任何已经成功初始化的资源来说,对其close()方法的相应调用是必需的。其要求严格遵守try/catch/finally块的用法,以此来确保任何从资源打开时起的执行路径最终都会到达一个关闭该资源的方法的调用。静态的分析工具,比如说FindBugs,在识别这一类的错误时会特别有帮助。然而通常的情况是,缺乏经验或是有经验的开发者都会写出错误的资源管理代码,即使最好的情况也是导致内存泄漏。

然而,应该承认,正如我们将要见到的那样,编写正确的资源代码需要许多嵌套的try/catch/finally块形式的样板代码。快速正确的编写出此类代码本身就是一个问题,与此同时,另一些编程语言,比如说PythonRuby,都已经提供了语言级别的设施,也就是通常所说的自动化资源管理来解决这一问题。

本文介绍了Java Platform, Standard Edition (Java SE) 7就自动化管理这一问题给出的解答方案,其以一种新的语言构造的形式作为Project Coin的一部分提出,这一语言构造被称为尝试使用资源(try-with-resources)语句。正如我们将要见到的那样,其已经远不止像Java SE 5的循环语句增强一样的仅是往语法中加入更多的糖这种情况。事实上,异常会掩盖彼此,使得很难调试识别出导致根源问题的原因。

在从Java开发者的角度介绍try-with-resources语句的本质要点之前,本文一开始先概述资源和异常的管理;然后说明类如何做好支持使用这类语句的准备;接下来,文章会讨论异常的掩盖以及Java SE 7做了怎样的改变来解决这些问题;最后,本文揭去了语言扩展背后的语法糖的神秘面纱,并在进行一个讨论后给出了最终的结论。

注:本文中描述的例子的源代码可从这里下载:sources.zip

 

 管理资源和异常

让我们先从下面摘选的一段代码开始:

private void incorrectWriting() throws IOException {
       DataOutputStream out = new DataOutputStream(new FileOutputStream("data"));
       out.writeInt(666);
       out.writeUTF("Hello");
       out.close();
}

乍一看,这一方法并没多大的危害:其打开一个名为data的文件,然后写入一个整数和一个串。java.io包中的流(stream)类的设计使得它们能够使用装饰器这一设计模式来进行组合使用。

作为例子,我们可以在DataOutputStream FileOutputStream之间加入一个压缩数据的输出流。当某个流被关闭时,它还关闭了它所装饰的流。再次回到这个例子中,当DataOutputStream实例上的close()被调用时,FileOutputStream上的close()也因此而被调用。

然而,关于这一方法中的对 close()方法的调用存在一个严重的问题。假设因为底层文件系统已没有空间,因此在写入整数或是串时抛出了一个异常,那么close()方法就没有机会被调用了。

DataOutputStream来说,这不是多大的问题,因为其只是在OutputStream实例上进行操作,对原始数据类型编码并把它们写入到字节数组中。真正的问题出在FileOutputStream身上,因为它在内部持有一个操作系统资源,一个文件描述符,只有在close()被调用时,该资源才能被释放。因此,这一方法泄漏了资源。

这一问题在短暂运行的程序中基本是无碍的,但在建立在Java Platform, Enterprise Edition (Java EE) 应用服务器之上的、长期运行的应用中则可能会导致整个服务器的必须重启,因为底层的操作系统所允许的打开的文件描述符的最大数目已经达到了。

一种正确地重写之前的方法的方式如下:

 private void correctWriting() throws IOException {
       DataOutputStream out = null;
       try {
           out = new DataOutputStream(new FileOutputStream("data"));
           out.writeInt(666);
           out.writeUTF("Hello");
       } finally {
           if (out != null) {
               out.close();
           }
     }        
}

在每一种情况下,抛出异常都会被传播给该方法的调用者,但try块之后的finally块确保了数据输出流的close()方法会被调用;相应的,这就确保了底下的文件输出流的close()方法也会被调用,最终的结果是正确地释放了与某个文件关联的操作系统资源。

为没有耐性的家伙提供的try-with-resources语句

无可否认,在前面的代码中存在着许多的样板代码,这些代码被用来确保资源被正确地关闭。随着流(stream)、网络套接口或是Java Database Connectivity (JDBC)连接的增多,这类样板代码会使方法中的业务逻辑越来越难读懂。更糟的是,它要求开发者的自律,因为编写错误的处理和不正确的资源关闭逻辑是一件很容易犯的事情。

与此同时,其他的编程语言已经引入了简化这类情况处理的构造,作为一个例子,前面的方法可以用Ruby来写成如下这个样子:

def writing_in_ruby
       File.open('rdata', 'w') do |f|
           f.write(666)
           f.write("Hello")
       end
end

而这一方法用Python可写成这个样子:

 def writing_in_python():
       with open("pdata", "w") as f:
           f.write(str(666))
           f.write("Hello")

Ruby中,File.open方法执行了一块代码,并确保即使是该块代码的执行出现了异常也会关闭打开的文件。

Python的例子相类似,特别是语句采用了一个有着close方法和一个代码块的对象,因此其也确保了不管是否有异常抛出,资源都能正确地关闭。

作为Project Coin的一部分,Java SE 7引入了类似的语言构造,该例子可被重写如下:

   private void writingWithARM() throws IOException {
       try (DataOutputStream out
               = new DataOutputStream(new FileOutputStream("data"))) {
           out.writeInt(666);
           out.writeUTF("Hello");
       }
   }

新的构造把try块扩展成了资源的声明,这很像是for循环的情况。任何在try块中声明的打开资源都会被关闭,因此,新的构造帮你屏蔽掉了需要成对出现的try块和finally块,相应的finally块被用来进行适当的资源管理。分号用来隔开每个资源,例如:

try (
       FileOutputStream out = new FileOutputStream("output");
       FileInputStream  in1 = new FileInputStream(“input1”);
       FileInputStream  in2 = new FileInputStream(“input2”)
   ) {
       // 使用这三个流来做一些有用的事情!
   }   // 任何情况下outin1in2都会被关闭

最后一点,这样的try-with-resources语句后面可以跟catchfinally块,就像Java SE 7之前的普通的try语句的做法那样。

制作一个可自动关闭的类

正如你也许已经猜到的那样,try-with-resources语句并不能够管理到每一个类。一个新的名为java.lang.AutoCloseable的接口被引入到了Java SE 7中。所有它要做的事情就是提供一个可以抛出一个受查异常(java.lang.Exception)的名为close()void方法,任何要加入到try-with-resources语句中的类都应该要实现这一接口。强烈建议实现的类和子接口声明一个比java.lang.Exception更精确的异常类型,或者甚至可以做得更好一些,如果调用close()应该不会导致失败的话,就完全不用声明异常类型。

这些close()方法已经被往回放入了标准的Java SE运行环境的许多类中,其中包括了java.iojava.niojavax.cryptojava.securityjava.util.zipjava.util.jarjavax.netjava.sql包。这一做法的主要优点是,现有的代码会继续像以前那样工作,而新的代码则可以很容易地利用到try-with-resources语句的好处。

    让我们来考虑一下下面的例子:

 public class AutoClose implements AutoCloseable {   
      
       @Override
       public void close() {
           System.out.println(">>> close()");
           throw new RuntimeException("Exception in close()");
       }
      
       public void work() throws MyException {
           System.out.println(">>> work()");
           throw new MyException("Exception in work()");
       }
      
       public static void main(String[] args) {
           try (AutoClose autoClose = new AutoClose()) {
               autoClose.work();
           } catch (MyException e) {
               e.printStackTrace();
           }
       }
   }
   class MyException extends Exception {
      
       public MyException() {
           super();
       }
      
       public MyException(String message) {
           super(message);
       }
   }
  

AutoClose类实现了AutoCloseable,因此其可被来作为try-with-resources语句的组成部分,正如main()方法中说明的那样。我们有意地加入了一些控制台输出,在该类的work()close()方法中抛出异常。运行这一程序会产生如下的输出:

>>> work()
   >>> close()
   MyException: Exception in work()
          at AutoClose.work(AutoClose.java:11)
          at AutoClose.main(AutoClose.java:16)
          Suppressed: java.lang.RuntimeException: Exception in close()
                 at AutoClose.close(AutoClose.java:6)
                 at AutoClose.main(AutoClose.java:17)

输出清楚地证明close()确实是在进入应该处理异常的catch块之前被调用了。然而,正在研究Java SE 7的开发者在看到前面标着“Suppressed: (…)”的异常栈跟踪行时可能会感到奇怪,其和close()方法抛出的异常是吻合的,但你却从未Java SE 7之前的版本中遇见过这种形式的栈跟踪内容。这是怎么一回事呢?

 异常的掩盖

为了理解前面例子中发生的事情,让我们暂时去掉try-with-resources语句,手工重写正确的资源管理代码。首先,让我提取出下面的被main方法调用的静态语句:

 public static void runWithMasking() throws MyException {
       AutoClose autoClose = new AutoClose();
       try {
           autoClose.work();
       } finally {
           autoClose.close();
       }
}

然后,让我们来相应地改造main方法:

 public static void main(String[] args) {
       try {
           runWithMasking();       
       } catch (Throwable t) {
           t.printStackTrace();
       }
}

现在,运行的程序会给出如下的输出:

>>> work()
   >>> close()
   java.lang.RuntimeException: Exception in close()
          at AutoClose.close(AutoClose.java:6)
          at AutoClose.runWithMasking(AutoClose.java:19)
          at AutoClose.main(AutoClose.java:52)

这一代码是Java SE 7之前正确资源管理的惯用做法,其说明了一个异常被另一个异常遮盖的问题。事实上,调用runWithMasking()方法的客户代码会收到close()方法抛出异常的通知,而实际的情况是,work()方法先抛出了异常。

然而,一次只能抛出一个异常,这意味着在处理异常时即使是正确的代码也会错过一些信息。当主要的异常被在关闭资源时进一步抛出的异常所掩盖时,开发者浪费了大量的时间来进行调试。聪明的读者可能会质疑这种说法,因为毕竟异常是可以嵌套的。但是,嵌套异常应该是用在有因果关系的异常之间的,通常的做法是,把低层面的异常包装在目标为应用架构的更高层面的异常中。一个很好的例子是JDBC驱动程序把套接口异常封装在了JDBC的连接中。而这里的例子真正是有两个异常:一个在work()中,另一个在close()中,它们之间绝对没有因果关系存在。

支持被压制的异常

因为异常掩盖在实际中是如此重要的一个问题,因此Java SE 7扩展了异常,这样被压制(suppressed的异常就可以附加到主异常上。我们前面所说的被掩盖(masked的异常实际上是一个被压制和被附加到主异常上的异常。

java.lang.Throwable的扩展如下:

1.      public final void addSuppressed(Throwable exception)把一个被压制的异常追加到另一个异常上,如此来避免异常掩盖。

2.      public final Throwable[] getSuppressed()获取被添加到一个异常中的受压制异常。

    这些扩展被特别引入,以用于支持try-with-resources语句并解决异常掩盖问题。

回到之前的runWithMasking()方法,让我们使用原来心中的对被压制异常的支持来重写该方法:

public static void runWithoutMasking() throws MyException {
       AutoClose autoClose = new AutoClose();
       MyException myException = null;
       try {
           autoClose.work();
       } catch (MyException e) {
           myException = e;
           throw e;
       } finally {
           if (myException != null) {
               try {
                   autoClose.close();
               } catch (Throwable t) {
                   myException.addSuppressed(t);
               }
           } else {
               autoClose.close();
           }
     }
}

很明显,这里给出的相当数量的代码只是为了正确地处理一个可自动关闭类的两个抛出异常的方法。一个局部变量被用来捕捉主要的异常,也即work()方法可能会抛出的那个异常。如果该异常被抛出的话,其会被捕捉,然后立即再次抛出,如此来把剩余的工作委托给finally块。

进入finally块后,指向主要异常的引用被检查,如果有异常抛出的话,则close()方法可能会抛出的异常就会作为被压制异常附加到该异常上。否则的话,close()方法被调用,且如果其抛出异常的话,那么该异常实际上就是主要异常,因此其不会掩盖了另一个异常。

让我们运行使用了这一新方法来进行修改的程序:

>>> work()

   >>> close()

   MyException: Exception in work()

          at AutoClose.work(AutoClose.java:11)

          at AutoClose.runWithoutMasking(AutoClose.java:27)

          at AutoClose.main(AutoClose.java:58)

          Suppressed: java.lang.RuntimeException: Exception in close()

                 at AutoClose.close(AutoClose.java:6)

                 at AutoClose.runWithoutMasking(AutoClose.java:34)

                 ... 1 more

 

正如你所见到的那样,我们手工重现了早先的try-with-resources语句的行为。

语法糖揭秘

我们实现的runWithoutMasking()方法通过正确地关闭资源以及防止异常的掩盖来重现了try-with-resources语句的行为。在现实情况中,Java编译器把下面方法的代码展开成与runWithoutMasking()的代码一致的情形,该方法使用了try-with-resources语句:

public static void runInARM() throws MyException {

       try (AutoClose autoClose = new AutoClose()) {

           autoClose.work();

       }

   }

这一点可通过反编译检查出来。虽然我们使用javap来比较字节码,其是Java Development Kit (JDK)二进制工具的组成部分,不过让我们把它当作一个字节码到Java源代码反编译器来使用。JD-GUI工具提取出来的runInARM()的代码如下(已重排格式):

public static void runInARM() throws MyException {

       AutoClose localAutoClose = new AutoClose();

       Object localObject1 = null;

       try {

           localAutoClose.work();

       } catch (Throwable localThrowable2) {

           localObject1 = localThrowable2;

           throw localThrowable2;

       } finally {

           if (localAutoClose != null) {

               if (localObject1 != null) {

                   try {

                       localAutoClose.close();

                   } catch (Throwable localThrowable3) {

                       localObject1.addSuppressed(localThrowable3);

                   }

               } else {

                   localAutoClose.close();

               }

           }

       }

   }

正如我们所见到的那样,我们手工编写的代码和编译器针对try-with-resources推断出来的代码有着相同的资源管理过程。还应该要注意的一点是,编译器处理了资源指针可能为空的情况,其在finally块中加入一个额外的if语句来检查给定的资源是否为空,以此来避免在一个空引用上调用close()时的空指针异常。我们在我们的手工实现中并未这样做,因为这一资源不可能为空。不过,编译器会系统地生成这一类的代码。

现在让我们考虑另一例子,这次涉及了三个资源:

private static void compress(String input, String output) throws IOException {

       try(

           FileInputStream fin = new FileInputStream(input);

           FileOutputStream fout = new FileOutputStream(output);

           GZIPOutputStream out = new GZIPOutputStream(fout)

       ) {

           byte[] buffer = new byte[4096];

           int nread = 0;

           while ((nread = fin.read(buffer)) != -1) {

               out.write(buffer, 0, nread);

           }

       }

   }

该方法操纵三个资源来压缩文件:一个读入流,一个压缩流和一个输出文件流。从资源管理的角度来说这一代码是正确的。在Java SE 7之前,你不得不编写一些类似于如下这一段的代码,这段代码是我们再次使用JD-GUI来反编译包含了这一方法的类得到的:

private static void compress(String paramString1, String paramString2)

           throws IOException {

       FileInputStream localFileInputStream = new FileInputStream(paramString1);

    Object localObject1 = null;

       try {

           FileOutputStream localFileOutputStream = new FileOutputStream(paramString2);

        Object localObject2 = null;

           try {

               GZIPOutputStream localGZIPOutputStream = new GZIPOutputStream(localFileOutputStream);

            Object localObject3 = null;

               try {

                   byte[] arrayOfByte = new byte[4096];

                   int i = 0;

                   while ((i = localFileInputStream.read(arrayOfByte)) != -1) {

                       localGZIPOutputStream.write(arrayOfByte, 0, i);

                   }

               } catch (Throwable localThrowable6) {

                   localObject3 = localThrowable6;

                   throw localThrowable6;

               } finally {

                   if (localGZIPOutputStream != null) {

                       if (localObject3 != null) {

                           try {

                               localGZIPOutputStream.close();

                           } catch (Throwable localThrowable7) {

                               localObject3.addSuppressed(localThrowable7);

                           }

                       } else {

                           localGZIPOutputStream.close();

                       }

                   }

               }

           } catch (Throwable localThrowable4) {

               localObject2 = localThrowable4;

               throw localThrowable4;

           } finally {

               if (localFileOutputStream != null) {

                   if (localObject2 != null) {

                       try {

                           localFileOutputStream.close();

                       } catch (Throwable localThrowable8) {

                           localObject2.addSuppressed(localThrowable8);

                       }

                   } else {

                       localFileOutputStream.close();

                   }

               }

           }

       } catch (Throwable localThrowable2) {

           localObject1 = localThrowable2;

           throw localThrowable2;

       } finally {

           if (localFileInputStream != null) {

               if (localObject1 != null) {

                   try {

                       localFileInputStream.close();

                   } catch (Throwable localThrowable9) {

                       localObject1.addSuppressed(localThrowable9);

                   }

               } else {

                   localFileInputStream.close();

               }

           }

       }

   }

 

对于这样的一个例子来说,Java SE 7中的try-with-resources语句所带来的好处是不言而喻的:要写的代码更少,代码更易于读懂,且最后但也同样重要的一点是,代码没有资源泄漏!

讨论

java.lang.AutoCloseable接口中的close()方法的定义提及了可能会抛出java.lang.Exception。然而,之前的AutoClose例子对这一方法的声明却并未提及任何的受查异常,这是我们有意为之的,部分是为了说明异常掩盖。

可自动关闭类的规范建议抛出的ava.lang.Exception要避免优先使用具体的受查异常,如果close()方法预期不会失败的话就不要提及任何的受查异常。其还建议不要声明任何不应该被压制的异常,java.lang.InterruptedException就是一个最好的例子。事实上,压制该异常并把它附加到另一异常上可能会导致线程中断事件被忽略,从而把应用置于一种不一致的状态中。

一个关于try-with-resources语句使用的合理问题是,相比较于手工编写的正确资源管理代码,其对性能的影响如何。而实际的情况是,不存在性能方面的影响,因为编译器为所有异常的适当处理推断出了数目尽可能少的正确代码,正如我们在前面的例子中通过反编译来说明的那样。

终于到最后要说的了,try-with-resources语句是语法糖,就像Java SE 5引入的用于扩展迭代器循环的循环增强一样。

不过话虽如此,我们却可以限制try-with-resources语句展开的复杂程度。一般来说,try块声明了越多的资源,生成的代码就会越复杂。前面的compress()方法可重写成仅使用两个资源而不是三个,结果是生成了更少的异常处理块:

private static void compress(String input, String output) throws IOException {

       try(

           FileInputStream fin = new FileInputStream(input);

           GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(output))

       ) {

           byte[] buffer = new byte[4096];

           int nread = 0;

           while ((nread = fin.read(buffer)) != -1) {

               out.write(buffer, 0, nread);

           }

       }

   }

正如在Java中出现try-with-resources语句之前的情况一样,一般经验是,开发者应该始终要明白在串接资源的实例化时需要做的取舍。为了做到这一点,最好的方法就是阅读每个资源的close()方法的规范来了解它们的语义和影响。

回到文章开始处的writingWithARM()例子上来,串接是安全的,因为DataOutputStream不可能在close()上抛出异常。然而,在最后的例子就不是这种情况,因为作为close()方法的一部分,GZIPOutputStream试图写入剩余的压缩数据。如果在写入压缩文件时一个异常被较早地抛了出来,GZIPOutputStream中的close()方法不仅也可能会抛出一个进一步的异常,而且会导致FileOutputStream中的close()方法不被调用,从而泄漏了一个文件描述符资源。

一种好的做法是在try-with-resources语句中为每一个持有关键系统资源的资源都做一个单独的声明,比如说一个文件描述符、一个套接口或是一个JDBC连接,在这些情况中你都必须要确保close()方法最终被调用。否则的话,所提供的相关资源API就要有这样的可能性,即串接的分配不仅是一种便利的方式:其在防止资源泄漏的同时还带来了更紧凑的代码。

结论

本文介绍了Java SE 7中一种新的用于安全资源管理的语言构造。这一扩展的影响不仅是更多的语法糖这么简单。事实上,其为开发者生成了正确的代码,消除了编写容易出错的样本代码这样不得已的需求。更重要的是,这一变化是与把一个异常附加到另一异常之上这种改进一起到来的,因此其为众所周知的异常彼此掩盖问题提供了一个优雅的解决方案。

其他参考

下面是附加的一些参考资料:

1.      Java SE 7 Preview: http://jdk7.java.net/preview/

2.      Java SE 7 API: http://download.java.net/jdk7/docs/api/ Original proposal for Automatic Resource Management by Joshua Bloch, February 27, 2009: http://mail.openjdk.java.net/pipermail/coin-dev/2009-February/000011.html and https://docs.google.com/View?id=ddv8ts74_3fs7483dp

3.      Project Coin: Updated ARM Spec, July 15, 2010: http://blogs.sun.com/darcy/entry/project_coin_updated_arm_spec

4.      Project Coin: JSR 334 EDR Now Available, January 11, 2010: http://blogs.sun.com/darcy/entry/project_coin_edr

5.      Project Coin: How to Terminate try-with-resources, January 31, 2011: http://blogs.sun.com/darcy/entry/project_coin_how_to_terminate

6.      Project Coin: try-with-resources on a Null Resource, February 16, 2011: http://blogs.sun.com/darcy/entry/project_coin_null_try_with

7.      Project Coin: JSR 334 in Public Review, March 24, 2011: http://blogs.sun.com/darcy/entry/project_coin_jsr_334_pr 

8.      Project Coin: http://openjdk.java.net/projects/coin/

9.      JSR334 early draft preview: http://jcp.org/aboutJava/communityprocess/edr/jsr334/index.html

10.  JSR334 public review: http://jcp.org/aboutJava/communityprocess/pr/jsr334/index.html

11.  Java Puzzlers: Traps, Pitfalls, and Corner Cases by Joshua Bloch and Neal Gafter (Addison-Wesley Professional, 2005)

12.  Effective Java Programming Language Guide by Joshua Bloch (Addison-Wesley Professional, 2001)

13.  The decorator design pattern: http://en.wikipedia.org/wiki/Decorator_pattern

14.  FindBugs, a static code analysis tool: http://findbugs.sourceforge.net/

15.  JD-GUI, a Java byte-code decompiler: http://java.decompiler.free.fr/?q=jdgui

16.  Python: http://www.python.org/

17.  Ruby: http://www.ruby-lang.org/

 

关于作者

    Julien Ponge是一位长期从事开源工作的技术高人。他创建了IzPack安装器框架,而且参加了其他几个项目,其中包括了与Sun Microsystems合作开发GlassFish应用服务器。他拥有UNSW SydneyUBP Clermont-Ferrand的计算机科学博士学位,他现在是INSA de Lyon计算机科学和工程系的副教授,且是INRIA Amazones团队的研究成员。他能够从行业和学术两个角度看问题,有很高的热情来进一步发展这些领域之间的协同作用。

 

转载注明地址:http://www.chengxuyuans.com/Java+/86286.html