【Java】ファイルクローズ漏れ発生時の挙動確認とクローズ漏れ対策

### 【概要】
`close`メソッドの実装忘れや`try-with-resources`で実装していない場合、ファイルクローズ漏れとなります。
ファイルクローズ漏れを放置するとファイルディスクリプタの上限を超えてしまい、それ以上ファイルが作成できないエラーが発生することを確認します。

Spring BootでファイルアップロードできるWebAPIを起動し、実際にファイルアップロードして確認していきます。

### 【環境】
Windows11
Docker version 20.10.21, build baeda1f
Java 19
Spring Boot 3.1.4

### 【コード】

https://github.com/nobu12/file-close-leak

<details><summary>DemoController.java</summary>

```java:DemoController.java
package com.example.demo.controller;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;

@RestController
public class DemoController {
    @PostMapping("/demo")
    public void getDemo(MultipartFile file, int n) throws IOException {
        // n = 1～2はファイルクローズ漏れのある実装
        // n = 3～4はファイルクローズ漏れのない実装
        if (n == 1) {
            Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
            Stream<Path> list = Files.list(tempDir);
        } else if (n == 2) {
            InputStream stream = file.getInputStream();
        } else if (n == 3) {
            Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
            try (Stream<Path> list = Files.list(tempDir)) {
            }
        } else if (n == 4) {
            try (InputStream stream = file.getInputStream()) {
            }
        }
    }
}
```

</details>



### 【挙動確認】

#### 準備

下記リンク先にあるファイルをローカルに保存してください。
本記事では`C:\work\demo\out\artifacts\demo_jar`配下に保存したパスで挙動確認しています。

https://github.com/nobu12/file-close-leak/tree/master/out/artifacts/demo_jar

ターミナル（本記事ではコマンドプロンプト）を4つ立ち上げて、下記の通りコマンドを実行していきます。

#### ターミナル1（Dockerイメージ・コンテナ作成・起動用）

Dockerイメージ・コンテナを作成して起動します。
jshellが起動しますが何もせずそのままにします。

```terminal
C:\work\demo\out\artifacts\demo_jar>docker build -t file-close-leak .
[+] Building 1.0s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                                        0.0s
 => => transferring dockerfile: 31B                                                         0.0s
 => [internal] load .dockerignore                                                           0.0s
 => => transferring context: 2B                                                             0.0s
 => [internal] load metadata for docker.io/library/eclipse-temurin:19-jdk-alpine            0.9s
 => [1/3] FROM docker.io/library/eclipse-temurin:19-jdk-alpine@sha256:7e4fc4b3ae1bd8ed4205  0.0s
 => [internal] load build context                                                           0.0s
 => => transferring context: 168B                                                           0.0s
 => CACHED [2/3] COPY demo.jar demo.jar                                                     0.0s
 => CACHED [3/3] COPY post-data*.txt /                                                      0.0s
 => exporting to image                                                                      0.0s
 => => exporting layers                                                                     0.0s
 => => writing image sha256:3c22e953d7a40a897d58e1985b76afce23574c9b546c771f471d8d675845fd  0.0s
 => => naming to docker.io/library/file-close-leak                                          0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

C:\work\demo\out\artifacts\demo_jar>
C:\work\demo\out\artifacts\demo_jar>docker run -it -p 8080:8080 --name file-close-leak file-close-leak
Oct 14, 2023 10:10:15 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
|  Welcome to JShell -- Version 19.0.2
|  For an introduction type: /help intro

jshell>
```

#### ターミナル2（ファイルディスクリプタの上限変更、アプリ立ち上げ用）

ファイルディスクリプタの上限を確認・変更できる`ulimit`コマンドを実行してきます。
変更前のファイルディスクリプタ上限値は1048576でしたが、挙動確認をしやすくするため30に変更しています。
（ファイルディスクリプタの変更とアプリ起動は同じターミナルで実行しないと、ファイルディスクリプタの変更が反映されませんでした）
後続処理でリクエスト実行した際にファイルディスクリプタ上限エラーとなった場合、一度アプリを落として再度起動すれば、ファイルオープン数はリセットされます。

```terminal
C:\work\demo\out\artifacts\demo_jar>docker container exec -it file-close-leak /bin/sh
/ # ulimit -n
1048576
/ # ulimit -n 30
/ # ulimit -n
30
/ #
/ # java -jar demo.jar

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.4)

2023-10-14T22:13:03.249Z  INFO 124 --- [           main] com.example.demo.DemoApplication
 : Starting DemoApplication using Java 19.0.2 with PID 124 (/demo.jar started by root in /)
2023-10-14T22:13:03.250Z  INFO 124 --- [           main] com.example.demo.DemoApplication
 : No active profile set, falling back to 1 default profile: "default"
2023-10-14T22:13:03.764Z  INFO 124 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer
 : Tomcat initialized with port(s): 8080 (http)
2023-10-14T22:13:03.770Z  INFO 124 --- [           main] o.apache.catalina.core.StandardService
 : Starting service [Tomcat]
2023-10-14T22:13:03.770Z  INFO 124 --- [           main] o.apache.catalina.core.StandardEngine
 : Starting Servlet engine: [Apache Tomcat/10.1.13]
2023-10-14T22:13:03.831Z  INFO 124 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]
 : Initializing Spring embedded WebApplicationContext
2023-10-14T22:13:03.831Z  INFO 124 --- [           main] w.s.c.ServletWebServerApplicationContext
 : Root WebApplicationContext: initialization completed in 555 ms
2023-10-14T22:13:04.091Z  INFO 124 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer
 : Tomcat started on port(s): 8080 (http) with context path ''
2023-10-14T22:13:04.093Z  INFO 124 --- [           main] com.example.demo.DemoApplication
 : Started DemoApplication in 1.019 seconds (process running for 1.153)

```

#### ターミナル3（ファイルオープン数の監視用）

demo.jarを起動しているプロセスが開いているファイルを監視します。
ここではプロセスIDが124であったため、`/proc/124/fd`ディレクトリを対象にしていますが、プロセスIDはご自身の環境に合わせてください。

```
C:\work\demo\out\artifacts\demo_jar>docker container exec -it file-close-leak /bin/sh
/ # ps | grep demo.jar | grep -v grep
  124 root      0:05 java -jar demo.jar
/ #
/ # watch -n 1 -d "ls -l /proc/124/fd | wc -l"
Every 1.0s: ls -l /proc/124/fd | wc -l                                       2023-10-14 22:20:06

12
```

#### ターミナル4（リクエスト実行用）

ファイルアップロードのリクエストを実行します。
テキストファイルについてはそれぞれ下記パターンの確認用となっているので、確認したいコードに合わせて実行してください。
post-data1.txt、post-data2.txtについてはリクエスト実行するごとに、ターミナル3のファイルオープン数が増加してきます。ファイルディスクリプタの上限値を30にしているので、30を超えるとターミナル2でエラーが発生します。
post-data3.txt、post-data4.txtについてはクローズ漏れをしないので、何回実行してもターミナル3のファイルオープン数が増えません。

- post-data1.txt：`Files.list`のクローズ漏れをするパターン
- post-data2.txt：`MultipartFile.getInputStream()`のクローズ漏れをするパターン
- post-data3.txt：`Files.list`のクローズ漏れをしないパターン
- post-data4.txt：`MultipartFile.getInputStream()`のクローズ漏れをしないパターン

```terminal
C:\work\demo\out\artifacts\demo_jar>docker container exec -it file-close-leak /bin/sh
/ # wget --header="Content-Type: multipart/form-data; boundary=--------------------------12345" --post-file=post-data1.txt http://localhost:8080/demo > /dev/null 2>&1
```


### 【ファイルディスクリプタの上限超過時のエラー】

ファイルディスクリプタの上限超過すると、ターミナル2でエラーが発生します。

#### `Files.list`のクローズ漏れをするパターンのエラー

`java.nio.file.FileSystemException: /tmp: No file descriptors available`エラーが発生しています。

<details><summary>エラーメッセージ</summary>

```java
2023-10-14T22:17:41.468Z ERROR 124 --- [nio-8080-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]
 : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.nio.file.FileSystemException: /tmp: No file descriptors available
        at java.base/sun.nio.fs.UnixException.translateToIOException(UnixException.java:100) ~[na:na]
        at java.base/sun.nio.fs.UnixException.asIOException(UnixException.java:115) ~[na:na]
        at java.base/sun.nio.fs.UnixFileSystemProvider.newDirectoryStream(UnixFileSystemProvider.java:436) ~[na:na]
        at java.base/java.nio.file.Files.newDirectoryStream(Files.java:482) ~[na:na]
        at java.base/java.nio.file.Files.list(Files.java:3791) ~[na:na]
        at com.example.demo.controller.DemoController.getDemo(DemoController.java:22) ~[demo.jar:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:578) ~[na:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[demo.jar:na]
        at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[demo.jar:na]
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[demo.jar:na]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[demo.jar:na]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[demo.jar:na]
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[demo.jar:na]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[demo.jar:na]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[demo.jar:na]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[demo.jar:na]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[demo.jar:na]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[demo.jar:na]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[demo.jar:na]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[demo.jar:na]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[demo.jar:na]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[demo.jar:na]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) ~[demo.jar:na]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[demo.jar:na]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[demo.jar:na]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) ~[demo.jar:na]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740) ~[demo.jar:na]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[demo.jar:na]
        at java.base/java.lang.Thread.run(Thread.java:1589) ~[na:na]
```

</details>


#### `MultipartFile.getInputStream()`のクローズ漏れをするパターンのエラー

`java.io.FileNotFoundException: /tmp/tomcat.8080.6074378283170779966/work/Tomcat/localhost/ROOT/upload_577ee3c3_e912_4314_9526_9ff273979e84_00000034.tmp (No file descriptors available)`エラーが発生しています。

<details><summary>エラーメッセージ</summary>

```java
2023-10-14T22:22:34.575Z ERROR 1155 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request] with root cause

java.io.FileNotFoundException: /tmp/tomcat.8080.6074378283170779966/work/Tomcat/localhost/ROOT/upload_577ee3c3_e912_4314_9526_9ff273979e84_00000034.tmp (No file descriptors available)
        at java.base/java.io.FileOutputStream.open0(Native Method) ~[na:na]
        at java.base/java.io.FileOutputStream.open(FileOutputStream.java:295) ~[na:na]
        at java.base/java.io.FileOutputStream.<init>(FileOutputStream.java:236) ~[na:na]
        at java.base/java.io.FileOutputStream.<init>(FileOutputStream.java:185) ~[na:na]
        at org.apache.tomcat.util.http.fileupload.DeferredFileOutputStream.thresholdReached(DeferredFileOutputStream.java:151) ~[demo.jar:na]
        at org.apache.tomcat.util.http.fileupload.ThresholdingOutputStream.checkThreshold(ThresholdingOutputStream.java:200) ~[demo.jar:na]
        at org.apache.tomcat.util.http.fileupload.ThresholdingOutputStream.write(ThresholdingOutputStream.java:126) ~[demo.jar:na]
        at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:103) ~[demo.jar:na]
        at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:292) ~[demo.jar:na]
        at org.apache.catalina.connector.Request.parseParts(Request.java:2788) ~[demo.jar:na]
        at org.apache.catalina.connector.Request.getParts(Request.java:2689) ~[demo.jar:na]
        at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:774) ~[demo.jar:na]
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:93) ~[demo.jar:na]
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:86) ~[demo.jar:na]
        at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:112) ~[demo.jar:na]
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1219) ~[demo.jar:na]
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1053) ~[demo.jar:na]
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914) ~[demo.jar:na]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590) ~[demo.jar:na]
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[demo.jar:na]
        at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[demo.jar:na]
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[demo.jar:na]
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[demo.jar:na]
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[demo.jar:na]
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[demo.jar:na]
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[demo.jar:na]
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[demo.jar:na]
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[demo.jar:na]
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[demo.jar:na]
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341) ~[demo.jar:na]
        at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391) ~[demo.jar:na]
        at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[demo.jar:na]
        at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894) ~[demo.jar:na]
        at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1740) ~[demo.jar:na]
        at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[demo.jar:na]
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[demo.jar:na]
        at java.base/java.lang.Thread.run(Thread.java:1589) ~[na:na]
```

</details>

### 【クローズ漏れ対策】

#### IDEの警告を適切に設定し見落とさない

##### Eclipseの場合

ウィンドウ -> 設定 -> Java -> コンパイラー -> エラー/警告　の設定画面で下記2つをエラーに設定します。
そうするとエディターでエラーが発生していることを確認でき、クローズ漏れを防ぐことができます。
デフォルトではそれぞれ警告と無視になっています。
- リソース・リーク
- 潜在的なリソース・リーク

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/e13709a8-26f7-8831-f32a-97a015bf7a49.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/c431fa40-af8d-087a-c9a9-7ff3c6183687.png)

##### IntelliJの場合

ファイル -> 設定 -> エディター -> インスペクション の設定でリソース管理に全てチェックを入れても`file.getInputStream`の方は警告が出ませんでしたので注意が必要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/d2968d4f-614e-1283-6ed2-128df37b59a3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/dcb503d9-b675-51f7-5f43-8b5d50d050a5.png)

ただし、[PMD](https://plugins.jetbrains.com/plugin/15412-pmd-idea)という静的解析プラグインを入れると検知できます（SpotBugsでは検知できませんでした）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/45edfc57-e9c4-3a0c-fc0f-e97a6c8f14ea.png)

#### 公式APIドキュメントを見る

公式APIドキュメントに注意書きが書いてあるので積極的に確認するようにします。

##### `Files.list`のドキュメント

https://docs.oracle.com/javase/jp/19/docs/api/java.base/java/nio/file/Files.html#list(java.nio.file.Path)

> このメソッドは、try-with-resources文または類似の制御構造内で使用して、ストリーム操作が完了した後にストリームのオープン・ディレクトリがすぐに閉じられるようにする必要があります。

##### `MultipartFile.getInputStream`のドキュメント

https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MultipartFile.html#getInputStream()

> ユーザーは、返されたストリームを閉じる責任があります。

#### ファイルオープン数を監視する

IDEや公式ドキュメントで確認漏れが発生しても、実際にアプリを起動してファイルオープン数を監視することにより、クローズ漏れを検知することができます。

##### Windowsの場合

Windows11の場合ですが、タスクマネージャーのリソースモニターから確認できます。
（検索ボックス右の更新ボタンを押さないと画面が更新されないことに注意してください）

###### `Files.list`のクローズ漏れをするパターン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/5421e81f-fb0b-2f9f-e50c-956c11a4b8d0.png)

###### `MultipartFile.getInputStream()`のクローズ漏れをするパターン

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/121997/c847cd0b-50d6-3094-8a96-6f3580f88755.png)


##### Linuxの場合

Linuxの場合は既に説明していますが、下記コマンドで該当プロセスのファイルオープン数を確認できます。

```terminal
watch -n 1 -d "ls -l /proc/[プロセスID]/fd | wc -l"
```

### 【参考サイト】
https://spring.pleiades.io/guides/topicals/spring-boot-docker/

https://qiita.com/nulltemp/items/364bb0937d28033b7885

https://qiita.com/okisanjp/items/d4e742da1992298be9f1

https://qiita.com/bsdhack/items/c124cc3ddc9faf9aa4c3