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

谁来背锅

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:

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();
	}
}

尤其是最后那句

The following example uses a finally block instead of a try-with-resources statement

让我误以为这两条代码是完全等价的。

由soot而来的疑惑

一个简单的例子

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执行完成之后

FileOutputStream os = new FileOutputStream(outFile)
try {
	...
} catch (IOException e) {
	...
} finally {
    os.close(); // 这里执行
    System.out.println("Out file: " + path);
    os.close(); // 或者这里执行
}

然而它生成的jimple

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

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代码:

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进行追加。

针对这种情况可以做一个简单的验证

public class Resource implements AutoCloseable {
    @Override
    public void close() {
        throw new RuntimeException("Resource");
    }
}
public class Main {
    public static void main(String[] args) {
        try (Resource r = new Resource()) {
            throw new RuntimeException("main");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行后打印的结果

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


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!