关于try-with-resources不知道的事

谁来背锅

try-with-resource是Java 1.7中新增的,来打开资源,而无需手动关闭语法糖。官方介绍如下:

1
2
3
4
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.

还十分贴心的贴上了说明代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:

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:

static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}

尤其是最后那句

1
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