背景

众所周知,所有被打开的系统资源,比如流、文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故。

在JDK7之前,只能通过 try-finally 手动判空并且手动关闭资源。JDK7之后,Java多了个新的语法:try-with-resources语句,对所有实现 java.lang.AutoCloseable 都可以自动关闭。极大的简化了代码。

术词表

简写全拼中文释义
ARM/armAutomatic Resource Management自动资源管理
JavaJava默认代表 Java 8
ScalaScala默认代表 Scala 2.13

使用

Java

  • 基于 Java8
  • 多个声明使用分号隔开,代码块终止时,无论是正常还是异常,将按照此顺序自动调用对象的 close 方法。 注意,资源的 close 方法与他们创建相反的顺序调用。
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
// 单资源
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
// 多资源
try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements(); ) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName =
((java.util.zip.ZipEntry) entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}

Scala

  • 需要 Scala 2.13,旧版 Scala 无 scala.util.{Try, Using}
  • 虽然 Scala 和 Java 写法不太一样,但是多个资源关闭的规律相同,即:资源的 close 方法与他们创建相反的顺序调用。
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
import java.io.{BufferedReader, FileReader}
import scala.util.{Try, Using}
// 单资源, 如果文件不存在异常会被抑制, 返回的是一个 Try[A] 类型的对象
val lines: Try[Seq[String]] =
Using(new BufferedReader(new FileReader("file.txt"))) { reader =>
Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq
}
// 单资源,抛出所有异常
val lines: Seq[String] =
Using.resource(new BufferedReader(new FileReader("file.txt"))) { reader =>
Iterator.continually(reader.readLine()).takeWhile(_ != null).toSeq
}
// 多资源
val lines: Try[Seq[String]] = Using.Manager { use =>
val r1 = use(new BufferedReader(new FileReader("file1.txt")))
val r2 = use(new BufferedReader(new FileReader("file2.txt")))
val r3 = use(new BufferedReader(new FileReader("file3.txt")))
val r4 = use(new BufferedReader(new FileReader("file4.txt")))

// use your resources here
def lines(reader: BufferedReader): Iterator[String] =
Iterator.continually(reader.readLine()).takeWhile(_ != null)

(lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList
}

异常抑制机制

Scala

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
==Suppression Behavior==

If two exceptions are thrown (e.g., by an operation and closing a resource),
one of them is re-thrown, and the other is
[[java.lang.Throwable#addSuppressed added to it as a suppressed exception]].
If the two exceptions are of different 'severities' (see below), the one of a higher
severity is re-thrown, and the one of a lower severity is added to it as a suppressed
exception. If the two exceptions are of the same severity, the one thrown first is
re-thrown, and the one thrown second is added to it as a suppressed exception.
If an exception is a [[scala.util.control.ControlThrowable `ControlThrowable`]], or
if it does not support suppression (see
[[java.lang.Throwable `Throwable`'s constructor with an `enableSuppression` parameter]]),
an exception that would have been suppressed is instead discarded.

Exceptions are ranked from highest to lowest severity as follows:
- `java.lang.VirtualMachineError`
- `java.lang.LinkageError`
- `java.lang.InterruptedException` and `java.lang.ThreadDeath`
- [[scala.util.control.NonFatal fatal exceptions]], excluding `scala.util.control.ControlThrowable`
- `scala.util.control.ControlThrowable`
- all other exceptions

When more than two exceptions are thrown, the first two are combined and
re-thrown as described above, and each successive exception thrown is combined
as it is thrown.
  • 以上是 Using 的 Scala doc, 简单翻译如下
1
2
3
4
5
6
7
8
9
10
11
12
如果有两个异常被抛出,最后只会有一个异常被重新抛出,另外的异常将会通过 java.lang.Throwable#addSuppressed 被作为一个抑制异常添加到重新抛出的异常里面。
如果异常不是属于同一严重性级别,严重性更高的异常将被抛出,严重性较低的作为抑制异常。
如果异常属于同一严重性级别,按先来先抛出来决定抛出哪个异常。如果一个异常不支持被抑制,则将会被忽略(具体看 java.lang.Throwable `Throwable` 构造函数里面的 `enableSuppression` 参数)

异常严重性,从高到低排列如下:
- `java.lang.VirtualMachineError`
- `java.lang.LinkageError`
- `java.lang.InterruptedException``java.lang.ThreadDeath`
- [[scala.util.control.NonFatal fatal exceptions]], 不包括 `scala.util.control.ControlThrowable`
- `scala.util.control.ControlThrowable`
- 其他异常
多个异常合并时,按FIFO两两处理,然后抛出最后合并过的异常。

Java

  • https://docs.oracle.com/javase/8/docs/technotes/guides/language/try-with-resources.html
  1. 如果 readLine 和 close 同时抛出异常,那么 finally 块里面的异常将会被抛出,其他异常将会被抑制
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
However, in this example, if the methods readLine and close both throw exceptions,
then the method readFirstLineFromFileWithFinallyBlock throws the exception thrown
from the finally block; the exception thrown from the try block is suppressed.
*/
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. 如果 try 和 try-with-res 块都抛出异常,那么 try 里面的异常将会被抛出,其他异常将会被抑制
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
/*
In contrast, in the example readFirstLineFromFile, if exceptions are thrown from
both the try block and the try-with-resources statement, then the method
readFirstLineFromFile throws the exception thrown from the try block; the exception
thrown from the try-with-resources block is suppressed. In Java SE 7 and later,
you can retrieve suppressed exceptions; see the section Suppressed Exceptions
for more information.
*/
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
// decompile
static String readFirstLineFromFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
Throwable var2 = null;
String var3;
try {
var3 = br.readLine();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (br != null) {
if (var2 != null) {
try {
br.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
br.close();
}
}
}
return var3;
}

  1. 任何和 try-with-res 关联的代码块都有可能抛出异常。以 writeToFileZipFileContents 为例,try 里面可以抛出一个异常,在关闭 ZipFile 和 BufferedWriter 时最多可以抛出两个异常。如果有多个异常同时抛出,try 代码块里面的为最终抛出的异常,try-with-res 的异常将会被抑制。
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
/**
* An exception can be thrown from the block of code associated with the try-with-resources statement. In the
* example writeToFileZipFileContents, an exception can be thrown from the try block, and up to two exceptions
* can be thrown from the try-with-resources statement when it tries to close the ZipFile and BufferedWriter objects.
*

* If an exception is thrown from the try block and one or more exceptions are thrown from the try-with-resources
* statement, then those exceptions thrown from the try-with-resources statement are suppressed, and the exception
* thrown by the block is the one that is thrown by the writeToFileZipFileContents method. You can retrieve these
* suppressed exceptions by calling the Throwable.getSuppressed method from the exception thrown by the try block.
*/
public static void writeToFileZipFileContents(String zipFileName, String outputFileName) throws java.io.IOException {
java.nio.charset.Charset charset = java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath = java.nio.file.Paths.get(outputFileName);
// Open zip file and create output file with try-with-resources statement
try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(zipFileName); java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outputFilePath, charset)) {
// Enumerate each entry
for (java.util.Enumeration entries = zf.entries(); entries.hasMoreElements(); ) {
// Get the entry name and write it to the output file
String newLine = System.getProperty("line.separator");
String zipEntryName = ((java.util.zip.ZipEntry) entries.nextElement()).getName() + newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
// decompile
public static void writeToFileZipFileContents(String zipFileName, String outputFileName) throws IOException {
Charset charset = StandardCharsets.US_ASCII;
Path outputFilePath = Paths.get(outputFileName);
ZipFile zf = new ZipFile(zipFileName);
Throwable var5 = null;
try {
BufferedWriter writer = Files.newBufferedWriter(outputFilePath, charset);
Throwable var7 = null;
try {
Enumeration entries = zf.entries();
while(entries.hasMoreElements()) {
String newLine = System.getProperty("line.separator");
String zipEntryName = ((ZipEntry)entries.nextElement()).getName() + newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
} catch (Throwable var32) {
var7 = var32;
throw var32;
} finally {
if (writer != null) {
if (var7 != null) {
try {
writer.close();
} catch (Throwable var31) {
var7.addSuppressed(var31);
}
} else {
writer.close();
}
}
}
} catch (Throwable var34) {
var5 = var34;
throw var34;
} finally {
if (zf != null) {
if (var5 != null) {
try {
zf.close();
} catch (Throwable var30) {
var5.addSuppressed(var30);
}
} else {
zf.close();
}
}
}
}

抑制后的异常会输出什么?

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
Exception exception = new Exception("Top Level Exception");
exception.printStackTrace();
System.out.println(exception);
System.out.println(exception.getMessage());
System.out.println("????");
System.err.println("????");
exception.addSuppressed(new Exception("suppress"));
exception.printStackTrace();
System.out.println(exception);
System.out.println(exception.getMessage());
/**
* 我们正常使用 printStackTrace() 的时候默认是输出到System.err中去的,而普通的输出都是放入System.out,
* 这两者都是对上层封装的输出流,在默认情况下两者是指向Console的文本流。所以两者可能会出现同步问题。
* 可以在printStackTrace()的时候指定输出流为System.out,通过回避System.err来实现Console中文本流的顺序问题。
*/
exception.printStackTrace(System.out);

/* output

java.lang.Exception: Top Level Exception
at ExceptionT.main(ExceptionT.java:7)
java.lang.Exception: Top Level Exception
at ExceptionT.main(ExceptionT.java:7)
Suppressed: java.lang.Exception: suppress
at ExceptionT.main(ExceptionT.java:12)
java.lang.Exception: Top Level Exception
Top Level Exception
????
java.lang.Exception: Top Level Exception
Top Level Exception
java.lang.Exception: Top Level Exception
at ExceptionT.main(ExceptionT.java:7)
Suppressed: java.lang.Exception: suppress
at ExceptionT.main(ExceptionT.java:12)
*/

Scala 2.11 实现 try-with-res

Scala 2.12 和 2.11 差别不大, 实现上是一样的

  • 通过前面的了解,我们已经初步熟悉了 try-with-res 的语法和机制,初步实现一下 Scala Using1
  • 需要注意的点
  • 多异常处理
  • 单个资源关闭
  • 多个资源关闭
  • Scalaify ?
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
import java.io.{FileInputStream, FileReader}

import scala.util.control.NonFatal

/**
* 对单个资源进行管理
*/
object UsingUtils {
/**
* Inspire by java try-with-res decode code and Scala 2.13 Using.apply
*/
def withResourcesNoException[T <: AutoCloseable, V](r: => T)(f: T => V): V = {
try {
withResources[T,V](r)(f)
}catch {
case e:Throwable =>
e.printStackTrace()
null.asInstanceOf[V]
}
}
def withResources[T <: AutoCloseable, V](r: => T)(f: T => V): V = {
val resource: T = r
require(resource != null, "resource is null")
var exception: Throwable = null
try {
f(resource)
} catch {
case NonFatal(e) =>
exception = e
throw e
} finally {
closeAndAddSuppressed(exception, resource)
}
}

private def closeAndAddSuppressed(e: Throwable,
resource: AutoCloseable): Unit = {
if (e != null) {
try {
resource.close()
} catch {
case NonFatal(suppressed) =>
e.addSuppressed(suppressed)
}
} else {
resource.close()
}
}

def main(args: Array[String]): Unit = {
// 若文件不存在,会直接报错
UsingUtils.withResources(new FileReader("/tmp/1"))(s => println(s.ready()))
UsingUtils.withResourcesNoException(new FileReader("/tmp/2"))(s => println(s.ready()))
UsingUtils.withResources(new FileReader("/tmp/2"))(s => println(s.ready()))
UsingUtils.withResources(null: FileInputStream)(s => println(s.available()))
}
}
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import java.io.{BufferedReader, FileReader}

import scala.util.control.{ControlThrowable, NonFatal}

/**
* 对多个资源进行管理
* @date 2022/02/09
* @note [[Using.Manager.apply]] is not same as Scala 2.13
*/
object Using {

/** A resource manager.
*
* Resources can be registered with the manager by calling [[acquire `acquire`]];
* such resources will be released in reverse order of their acquisition
* when the manager is closed, regardless of any exceptions thrown
* during use.
*
* $suppressionBehavior
*
* @note It is recommended for API designers to require an implicit `Manager`
* for the creation of custom resources, and to call `acquire` during those
* resources' construction. Doing so guarantees that the resource ''must'' be
* automatically managed, and makes it impossible to forget to do so.
*
*
* Example:
* {{{
* class SafeFileReader(file: File)(implicit manager: Using.Manager)
* extends BufferedReader(new FileReader(file)) {
*
* def this(fileName: String)(implicit manager: Using.Manager) = this(new File(fileName))
*
* manager.acquire(this)
* }
* }}}
*/
final class Manager private {

import Manager._

private var closed = false
private[this] var resources: List[Resource[_]] = Nil

/** Registers the specified resource with this manager, so that
* the resource is released when the manager is closed, and then
* returns the (unmodified) resource.
*/
def apply[R: Releasable](resource: R): R = {
acquire(resource)
resource
}

/** Registers the specified resource with this manager, so that
* the resource is released when the manager is closed.
*/
def acquire[R: Releasable](resource: R): Unit = {
if (resource == null) throw new NullPointerException("null resource")
if (closed) throw new IllegalStateException("Manager has already been closed")
resources = new Resource(resource) :: resources
}

private def manage[A](op: Manager => A): A = {
var toThrow: Throwable = null
try {
op(this)
} catch {
case t: Throwable =>
toThrow = t
null.asInstanceOf[A] // compiler doesn't know `finally` will throw
} finally {
closed = true
var rs: List[Resource[_]] = resources
resources = null // allow GC, in case something is holding a reference to `this`
while (rs.nonEmpty) {
val resource: Resource[_] = rs.head
rs = rs.tail
try resource.release()
catch {
case t: Throwable =>
if (toThrow == null) toThrow = t
else toThrow = preferentiallySuppress(toThrow, t)
}
}
if (toThrow != null) throw toThrow
}
}
}

object Manager {
/** Performs an operation using a `Manager`, then closes the `Manager`,
* releasing its resources (in reverse order of acquisition).
*
* Example:
* {{{
* val lines = Using.Manager { use =>
* use(new BufferedReader(new FileReader("file.txt"))).lines()
* }
* }}}
*
* If using resources which require an implicit `Manager` as a parameter,
* this method should be invoked with an `implicit` modifier before the function
* parameter:
*
* Example:
* {{{
* val lines = Using.Manager { implicit use =>
* new SafeFileReader("file.txt").lines()
* }
* }}}
*
* See the main doc for [[Using `Using`]] for full details of suppression behavior.
*
* @param op the operation to perform using the manager
* @tparam A the return type of the operation
*
* @return a [[Try]] containing an exception if one or more were thrown,
* or the result of the operation if no exceptions were thrown
*/
def apply[A](op: Manager => A): A = try {
(new Manager).manage(op)
} catch {
case e: Throwable =>
e.printStackTrace()
null.asInstanceOf[A]
}

private final class Resource[R](resource: R)(implicit releasable: Releasable[R]) {
def release(): Unit = releasable.release(resource)
}

private def preferentiallySuppress(primary: Throwable, secondary: Throwable): Throwable = {
def score(t: Throwable): Int = t match {
case _: VirtualMachineError => 4
case _: LinkageError => 3
case _: InterruptedException | _: ThreadDeath => 2
case _: ControlThrowable => 0
case e if !NonFatal(e) => 1 // in case this method gets out of sync with NonFatal
case _ => -1
}

@inline def suppress(t: Throwable, suppressed: Throwable): Throwable = {
t.addSuppressed(suppressed);
t
}

if (score(secondary) > score(primary)) suppress(secondary, primary)
else suppress(primary, secondary)
}
}

/** A type class describing how to release a particular type of resource.
*
* A resource is anything which needs to be released, closed, or otherwise cleaned up
* in some way after it is finished being used, and for which waiting for the object's
* garbage collection to be cleaned up would be unacceptable. For example, an instance of
* [[java.io.OutputStream]] would be considered a resource, because it is important to close
* the stream after it is finished being used.
*
* An instance of `Releasable` is needed in order to automatically manage a resource
* with [[Using `Using`]]. An implicit instance is provided for all types extending
* [[java.lang.AutoCloseable]].
*
* @tparam R the type of the resource
*/
trait Releasable[-R] {
/** Releases the specified resource. */
def release(resource: R): Unit
}

object Releasable {
/** An implicit `Releasable` for [[java.lang.AutoCloseable `AutoCloseable`s]]. */
implicit object AutoCloseableIsReleasable extends Releasable[AutoCloseable] {
def release(resource: AutoCloseable): Unit = resource.close()
}
}

def main(args: Array[String]): Unit = {
val lines: List[String] = Using.Manager.apply { use =>
val r1 = use(new BufferedReader(new FileReader("/tmp/file1.txt")))
val r2 = use(new BufferedReader(new FileReader("/tmp/file2.txt")))
val r3 = use(new BufferedReader(new FileReader("/tmp/file3.txt")))
val r4 = use(new BufferedReader(new FileReader("/tmp/file4.txt")))

// use your resources here
def lines(reader: BufferedReader): Iterator[String] =
Iterator.continually(reader.readLine()).takeWhile(_ != null)

(lines(r1) ++ lines(r2) ++ lines(r3) ++ lines(r4)).toList
}
println(lines)
}
}

开源实现

  • https://github.com/pathikrit/better-files/
  • https://github.com/jsuereth/scala-arm/
名称https://github.com/jsuereth/scala-armhttps://github.com/pathikrit/better-files
链接https://github.com/jsuereth/scala-arm/https://github.com/pathikrit/better-files
数据采集时间2022.2.102022.2.10
Star5531400+
Commits194978
Last Commit2020.10.262021.4.24
Total Issues41197
Total PR76348
SupportsARMARM, I/O, Resource APIs, Streams, File Monitoring…

scala-arm

  • Only a simple example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import resource._
// Copy input into output.
for {
input <- managed(new java.io.FileInputStream("test.txt"))
output <- managed(new java.io.FileOutputStream("test2.txt"))
} {
val buffer = new Array[Byte](512)
def read(): Unit = input.read(buffer) match {
case -1 => ()
case n =>
output.write(buffer,0,n)
read()
}
read()
}

Better Files

  1. Simple I/O
  2. Resource APIs
  3. Streams
  4. Encodings
  5. Java serialization utils
  6. Java compatibility
  7. Globbing
  8. File system operations
  9. Checksums
  10. Temporary files
  11. UNIX DSL
  12. File attributes
  13. File comparison
  14. Zip/GZip
  15. Automatic Resource Management
  16. Scanner
  17. File Monitoring
  18. Reactive File Watcher

Q

既然你都看到这里了, 那我留个问题吧:

  • 比如带 AutoClose 的文件资源,我可以仅调用但不进行 close 操作吗?它会自动关闭吗?