谁来背锅 try-with-resource是Java 1.7中新增的,来打开资源,而无需手动关闭语法糖。官方介绍如下:
The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.
还十分贴心的贴上了说明代码
The following example reads the first line from a file. It uses an instance of BufferedReader to read data from the file. BufferedReader is a resource that must be closed after the program is finished with it:
1 2 3 4 5 static String readFirstLineFromFile (String path) throws IOException { try (BufferedReader br = new BufferedReader (new FileReader (path))) { return br.readLine(); } }
In this example, the resource declared in the try-with-resources statement is a BufferedReader. The declaration statement appears within parentheses immediately after the try keyword. The class BufferedReader, in Java SE 7 and later, implements the interface java.lang.AutoCloseable. Because the BufferedReader instance is declared in a try-with-resource statement, it will be closed regardless of whether the try statement completes normally or abruptly (as a result of the method BufferedReader.readLine throwing an IOException).
Prior to Java SE 7, you can use a finally block to ensure that a resource is closed regardless of whether the try statement completes normally or abruptly. The following example uses a finally block instead of a try-with-resources statement:
1 2 3 4 5 6 7 8 static String readFirstLineFromFileWithFinallyBlock (String path) throws IOException { BufferedReader br = new BufferedReader (new FileReader (path)); try { return br.readLine(); } finally { if (br != null ) br.close(); } }
尤其是最后那句
The following example uses a finally block instead of a try-with-resources statement
让我误以为这两条代码是完全等价的。
由soot而来的疑惑 一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.google.common.flogger.FluentLogger;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.IOException;public class FinalTest { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); public static void finalTest (String path, String[] contant) { File outFile = new File (path); try (FileOutputStream os = new FileOutputStream (outFile)) { for (String s : contant) { os.write(s.getBytes()); } } catch (IOException e) { logger.atSevere().withCause(e).log(); } finally { System.out.println("Out file: " + path); } } }
按照文档中的例子,close函数等价于在finally中执行,也就是说在exception block执行完成之后
1 2 3 4 5 6 7 8 9 10 FileOutputStream os = new FileOutputStream (outFile)try { ... } catch (IOException e) { ... } finally { os.close(); System.out.println("Out file: " + path); os.close(); }
然而它生成的jimple
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public static void finalTest(java.lang.String, java.lang.String[]) { ... ... label08: if $r8 == null goto label13; if r32 == null goto label12; label09: virtualinvoke $r8.<java.io.FileOutputStream: void close()>(); label10: goto label13; label11: $r23 := @caughtexception; virtualinvoke r32.<java.lang.Throwable: void addSuppressed(java.lang.Throwable)>($r23); goto label13; label12: virtualinvoke $r8.<java.io.FileOutputStream: void close()>(); label13: throw $r21; ... ... label15: // exception label $r10 := @caughtexception; $r11 = <com.sbrella.test.FinalTest: com.google.common.flogger.FluentLogger logger>; $r12 = virtualinvoke $r11.<com.google.common.flogger.FluentLogger: com.google.common.flogger.LoggingApi atSevere()>(); $r13 = (com.google.common.flogger.FluentLogger$Api) $r12; $r14 = interfaceinvoke $r13.<com.google.common.flogger.FluentLogger$Api: com.google.common.flogger.LoggingApi withCause(java.lang.Throwable)>($r10); $r15 = (com.google.common.flogger.FluentLogger$Api) $r14; interfaceinvoke $r15.<com.google.common.flogger.FluentLogger$Api: void log()>(); label16: $r17 = <java.lang.System: java.io.PrintStream out>; $r16 = new java.lang.StringBuilder; specialinvoke $r16.<java.lang.StringBuilder: void <init>()>(); $r18 = virtualinvoke $r16.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>("Out file: "); $r19 = virtualinvoke $r18.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r0); $r20 = virtualinvoke $r19.<java.lang.StringBuilder: java.lang.String toString()>(); virtualinvoke $r17.<java.io.PrintStream: void println(java.lang.String)>($r20); goto label19; label17: $r25 := @caughtexception; label18: $r27 = <java.lang.System: java.io.PrintStream out>; $r26 = new java.lang.StringBuilder; specialinvoke $r26.<java.lang.StringBuilder: void <init>()>(); $r28 = virtualinvoke $r26.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>("Out file: "); $r29 = virtualinvoke $r28.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r0); $r30 = virtualinvoke $r29.<java.lang.StringBuilder: java.lang.String toString()>(); virtualinvoke $r27.<java.io.PrintStream: void println(java.lang.String)>($r30); throw $r25; label19: return; }
只有一个exception label,且没有任何跳转。也就是说在exception执行完之后直接执行finally中的println,然后便退出了。
那close呢,在exception之前就执行了。
Bytecode的答案 要知道真正的执行顺序自然是到class文件中寻找答案,直接看bytecode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 public static void finalTest(java.lang.String, java.lang.String[]); descriptor: (Ljava/lang/String;[Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=3, locals=12, args_size=2 ... ... 49: invokevirtual #9 // Method java/lang/String.getBytes:()[B 52: invokevirtual #10 // write 函数触发异常 java/io/FileOutputStream.write:([B)V 55: iinc 7, 1 58: goto 32 61: aload_3 62: ifnull 142 65: aload 4 67: ifnull 89 70: aload_3 71: invokevirtual #11 // Method java/io/FileOutputStream.close:()V 74: goto 142 77: astore 5 79: aload 4 81: aload 5 83: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V 86: goto 142 89: aload_3 90: invokevirtual #11 // Method java/io/FileOutputStream.close:()V 93: goto 142 96: astore 5 // 根据exception table,write的异常跳转到这里 98: aload 5 100: astore 4 102: aload 5 104: athrow // throw 一个异常在并跳转到105 105: astore 9 107: aload_3 // 取出FileOutputStream 108: ifnull 139 111: aload 4 // 4中保存的是Exception变量 113: ifnull 135 116: aload_3 117: invokevirtual #11 // Method java/io/FileOutputStream.close:()V 120: goto 139 123: astore 10 125: aload 4 127: aload 10 129: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V 132: goto 139 135: aload_3 136: invokevirtual #11 // Method java/io/FileOutputStream.close:()V 139: aload 9 141: athrow // 最终在这里再次throw 142: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 145: new #15 // class java/lang/StringBuilder 148: dup 149: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V 152: ldc #17 // String Out file: 154: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 157: aload_0 158: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 161: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 164: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 167: goto 252 170: astore_3 // 由于是IOException,在这里被捕获 171: getstatic #22 // Field logger:Lcom/google/common/flogger/FluentLogger; 174: invokevirtual #23 // Method com/google/common/flogger/FluentLogger.atSevere:()Lcom/google/common/flogger/LoggingApi; 177: checkcast #24 // class com/google/common/flogger/FluentLogger$Api 180: aload_3 181: invokeinterface #25, 2 // InterfaceMethod com/google/common/flogger/FluentLogger$Api.withCause:(Ljava/lang/Throwable;)Lcom/google/common/flogger/LoggingApi; 186: checkcast #24 // class com/google/common/flogger/FluentLogger$Api 189: invokeinterface #26, 1 // InterfaceMethod com/google/common/flogger/FluentLogger$Api.log:()V 194: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 197: new #15 // class java/lang/StringBuilder 200: dup 201: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V 204: ldc #17 // String Out file: 206: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 209: aload_0 210: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 213: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 216: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 219: goto 252 222: astore 11 224: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream; 227: new #15 // class java/lang/StringBuilder 230: dup 231: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V 234: ldc #17 // String Out file: 236: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 239: aload_0 240: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 243: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 246: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 249: aload 11 251: athrow 252: return Exception table: from to target type 70 74 77 Class java/lang/Throwable 21 61 96 Class java/lang/Throwable 21 61 105 any 116 120 123 Class java/lang/Throwable 96 107 105 any 9 142 170 Class java/io/IOException 9 142 222 any 170 194 222 any 222 224 222 any
直接假设write函数触发IOException,模拟执行一下。
从Exception Table中看到,write函数的异常会去往96的astore指令,然后在104通过指令athrow触发异常并去往105。局部变量表中第3个保存的就是FileOutputStream的this指针,通过aload_3获取并判断是否为null,4中保存的是catch到的Exception,是否为null。如果this指针不为空则调用close函数。之后会在141重新throw,由于该Exception是IOException,所以会被170处捕获,也就是进入了Java源代码中的exception block。
根据上述的执行流程,更接近于以下的Java代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void finalTest (String path, String[] contant) { File outFile = new File (path); try { FileOutputStream os = new FileOutputStream (outFile); try { for (String s : contant) { os.write(s.getBytes()); } } catch (Throwable e) { if (os != null ) { os.close(); } throw e; } } catch (IOException e) { logger.atSevere().withCause(e).log(); } finally { System.out.println("Out file: " + path); } }
注意os.close()本身也是会throw IOException。根据Exception Table,bytecode中117处的那次close调用如果触发异常则会跳转到123,调用addSuppressed进行追加。
针对这种情况可以做一个简单的验证
1 2 3 4 5 6 public class Resource implements AutoCloseable { @Override public void close () { throw new RuntimeException ("Resource" ); } }
1 2 3 4 5 6 7 8 9 public class Main { public static void main (String[] args) { try (Resource r = new Resource ()) { throw new RuntimeException ("main" ); } catch (Exception e) { e.printStackTrace(); } } }
执行后打印的结果
1 2 3 4 5 java.lang.RuntimeException: main at Main.main(Main.java:4) Suppressed: java.lang.RuntimeException: Resource at Resource.close(Resource.java:4) at Main.main(Main.java:5)
文档链接
https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html