Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

バイナリファイルをアップロードするときのエンコーディング

  • Q: ブラウザからバイナリファイルをPOSTリクエストでアップロードするとき、HTTPリクエストボディ上ではどういう見た目になるんだっけ? URLエンコードされる?Base64になる?
  • A: バイナリがそのまま渡される。ChromeのAdvanced REST Clientで確認。こんな感じ。
    f:id:satob:20210926150133p:plain
    Advanced REST Clientでバイナリファイルをアップロードしたときの見た目

Spring Frameworkのmax-file-sizeによるアップロードファイルサイズ制限のまとめ

satob.hatenablog.com

  • application.propertiesspring.servlet.multipart.max-file-size=100のように設定することでサイズ制限設定可能。
    • Spring Frameworkのファイルアップロード機能は、Webコンテナが持っているmultipartリクエストの処理機能をそのまま使う*1。ファイルサイズ上限のチェックに引っかかったときのスタックトレースを見ると、Tomcatではorg.apache.tomcat.util.http.fileuploadパッケージが使われている。これにはcommons-fileuploadからコピーされてきたコードが使われている*2
    • Webコンテナのmultipartリクエストのサイズ制限機能は、web.xmlからも設定できる*3。プログラム内でも指定はできるが、指定をするためのMultipartConfigElementはSpringアプリケーション1つに対して1個のようなので*4、Controllerごと・URLごとのファイルサイズ制限は行えない模様*5
    • URLごとのファイルサイズ制限を指定するアノテーションググる@MultipartConfigがある。これはJavaEE仕様(javax.servlet.annotation.MultipartConfig)なので、少なくともSpringだけでは利用できない模様*6
  • max-file-sizeによって発生したエラーは、Controllerではハンドリングできない。URLごとに個別のファイルサイズ制限をしたい、またはファイルサイズ上限エラーのハンドリングをビジネスロジックの中で行い、ビジネスロジックのエラーとしてレスポンスに含めたいという場合は、MultipartFile#getSize()を呼び出して自分で判定を行う必要がある*7。例外としてはSpring Frameworkorg.springframework.web.multipart.MaxUploadSizeExceededExceptionを提供しているのでこれを使えばよい。
  • application.propertiesfile-size-thresholdがデフォルトなら、アップロードされたファイルは常に一時ファイルへ格納されJavaヒープ上には格納されないので、大きいファイルをアップロードされてJavaのヒープが溢れるといったことはなさそう。
  • 一時ファイルはリクエストが処理されたらすぐに削除されるので、ハウスキープ処理は基本的に必要なさそう。
  • 一時ファイルが作られる先は、OSのテンポラリディレクトリの下のtomcat.【ポート番号】.【Tomcatを立ち上げるたびに決まる乱数?】\work\Tomcat\【ホスト名】\以下。ファイル削除が必要な場合はそこを削除する。
01: POST /upload HTTP/1.1
02: HOST: localhost:8080
03: content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
04: content-length: 500
05: 
06: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
07: Content-Disposition: form-data; name="hoge"; filename="blob"
08: Content-Type: text/plain
09: 
10: piyo
11: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
12: Content-Disposition: form-data; name="file"; filename="100.txt"
13: Content-Type: text/plain
14: Content-Length: 150
15: 
16: 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890............................................................
17: ------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--
  • 上記のようなリクエストがあった場合、以下のように処理が行われる。
    • 4行目のContent-Lengthは必須。これがなかった場合、multipartリクエストとして扱われない(ファイル部分は無視されてしまう)。
    • 4行目のContent-Lengthからファイル部分のサイズを逆算するなどはしない。
    • 14行目のContent-Lengthはあってもなくてもよい。ChromeのAdvanced REST Clientの場合、14行目のContent-Lengthは付与されない。
    • 14行目のContent-Lengthがある場合、これを読み込んだ時点でmax-file-sizeとの比較が行われる。パスしなかった場合は即エラーとなり、以降のデータは読み込まれない。
    • 14行目のContent-Lengthがない場合は、16行目以降の内容を読み込み始める。max-file-sizeを超える内容を読み込んだ時点で即エラーとなる。エラーとなった場合、以降のデータは読み込まれない。
    • 14行目のContent-Lengthがあって、かつmax-file-sizeよりも小さい場合は、Content-Lengthがない場合と同じ処理になる。
    • 上記で「即エラーとなる」というのは、101バイト目を読み込んだ時点でエラーとなるわけではなく、もう少し(数十バイト?)読み込んでからエラーが返る。

Spring Frameworkのアップロードファイルサイズ制限のテスト

spring.servlet.multipart.max-file-size=100を設定した状態で、以下のようなControllerに様々なmultipart/form-dataリクエストを送信して結果を比較した。

@PostMapping("/upload")
public String handleFileUpload(@RequestParam("file") MultipartFile file, Model model,
        RedirectAttributes redirectAttributes) {

    System.out.println(file.getOriginalFilename());

    return "redirect:/";
}

これにより、以下のような不明点の調査を行った。

  • 大きいファイルをアップロードした際にJavaヒープがあふれないか?
  • 大きいファイルをアップロードした際のエラーハンドリングを個別のControllerで行えるか?
  • 大きいファイルをアップロードした際に、アップロードが終わるまでエラー処理には入らないのか?
  • ファイルをアップロードした際に一時ファイルが作られる場合、一時ファイルの削除などハウスキープ処理は必要か? 必要な場合、対象のディレクトリはどこか?
  • ファイルアップロードを行う場合、リクエストに必須のヘッダは何か?

1. ファイルサイズ上限以下のファイルを送信した場合

リクエス

リクエストはChromeのAdvanced REST clientで作成した。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 419

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • 正常に処理され、HTTPレスポンス200が返る。
  • Controllerのメソッドが実行される(100.txtがコンソールに出力される)。
  • OSのテンポラリディレクトリ以下にtomcat.8080.6089307547608376372\work\Tomcat\localhost\ROOT\upload_68a5f1c0_fdc2_45d4_8705_18cd81766440_00000023.tmpのような一時ファイルが作られて、そこにファイルの内容が格納される。ファイルの内容はJavaヒープ上には読みこまれない*1
  • 一時ファイルは、リクエストの処理が完了して200レスポンスが返ると削除される。

分かったこと

  • file-size-thresholdがデフォルトなら、大きいファイルをアップロードされてJavaのヒープが溢れるといったことはなさそう。
  • 一時ファイルはリクエストが処理されたらすぐに削除されるので、ハウスキープ処理は基本的に必要なさそう。
  • 一時ファイルが作られる先は、OSのテンポラリディレクトリの下のtomcat.【ポート番号】.【Tomcatを立ち上げるたびに決まる乱数?】\work\Tomcat\【ホスト名】\以下。ファイル削除が必要な場合はそこを削除する。

2. ファイルサイズ上限を超えるファイルを送信した場合

リクエス

リクエストはChromeのAdvanced REST clientで作成した。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryoCEYONcK9AYZ4G7B
content-length: 420

------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="file"; filename="101.txt"
Content-Type: text/plain

12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
------WebKitFormBoundaryoCEYONcK9AYZ4G7B--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • max-file-sizeによって発生したエラーは、Controllerではハンドリングできない。エラーメッセージをHTML中に含めて返したい、といった場合には使いにくそう。ファイルアップロードのリクエストはajaxで送るとかであれば大きな問題はないが……。

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:114) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

3. リクエスト全体のContent-Lengthヘッダを省略し、ファイル側にContent-Lengthヘッダを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryoCEYONcK9AYZ4G7B

------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryoCEYONcK9AYZ4G7B
Content-Disposition: form-data; name="file"; filename="101.txt"
Content-Type: text/plain
content-length: 100

12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901
------WebKitFormBoundaryoCEYONcK9AYZ4G7B--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • multipart/form-dataリクエストとして認識されない。
  • 標準出力へ以下のエラーが出力される。スタックトレースは出力されない。
2021-09-26 00:35:15.559  WARN 4920 --- [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present]

分かったこと

  • ファイルをアップロードする場合、リクエスト全体のContent-Lengthは必須。

4. リクエストの本当の長さより短かいContent-Lengthを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 200

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • 6.や7.の例と異なり、telnetを実行しているコンソールに入力内容があふれることはない。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • リクエスト全体のContent-Lengthを認識した上で、以降の内容の読み込み処理が行われている。

スタックトレース

org.apache.tomcat.util.http.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly
    at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.makeAvailable(MultipartStream.java:981) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.MultipartStream$ItemInputStream.read(MultipartStream.java:879) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:133) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:132) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

5. リクエスト全体とファイルの両方にContent-Lengthを指定した場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 441

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain
content-length: 100

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • 正常に処理され、HTTPレスポンス200が返る。
  • Controllerのメソッドが実行される(100.txtがコンソールに出力される)。

分かったこと

  • multipartのファイル部分に個別にContent-Lengthを指定しても問題ない。

6. ファイルのContent-Lengthをmax-file-sizeより長い値にした場合

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。ファイル本体の長さはmax-file-sizeを超えず、Content-Lengthだけがmax-file-sizeを超えた値になっている。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 441

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain
content-length: 101

123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • リクエストのうち「45678901234567890123456789012345678901234567890123456789012345678901234567890123456789」はサーバへ送信されず、telnetを実行しているコンソールの標準出力にあふれる。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • multipartのファイル部分に個別に指定したContent-Lengthを認識した上で以降の読み込み処理行われている。
  • リクエストを最後まで読んでからContent-Lengthを確認するのではなく、Content-Lengthを読み込んだ時点でmax-file-sizeのエラー処理に入っている。
  • ファイル個別のContent-Lengthの行が送られた後、すこし間があいてからエラー処理が行われるのは、リクエストを読み込むStreamのバッファのせい?

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl.<init>(FileItemStreamImpl.java:95) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.findNextItem(FileItemIteratorImpl.java:247) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.impl.FileItemIteratorImpl.hasNext(FileItemIteratorImpl.java:297) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:283) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

7. max-file-sizeより大きいファイルを送信した際にエラーが返るタイミング

リクエス

1.や2.のリクエストをもとに作成した以下のリクエストをtelnetで流しこんだ。

POST /upload HTTP/1.1
HOST: localhost:8080
content-type: multipart/form-data; boundary=----WebKitFormBoundaryqx0dK5IFp8lW2Hjr
content-length: 479

------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="hoge"; filename="blob"
Content-Type: text/plain

piyo
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr
Content-Disposition: form-data; name="file"; filename="100.txt"
Content-Type: text/plain

1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890............................................................
------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--

結果

  • エラーとなり、HTTPレスポンス500が返る。
  • Controllerのメソッドは実行されない(Controllerまで処理が届かない)。
  • リクエストのうち「「...(改行)------WebKitFormBoundaryqx0dK5IFp8lW2Hjr--」はサーバへ送信されず、telnetを実行しているコンソールの標準出力にあふれる。
  • 標準出力へ以下のスタックトレースが出力される。

分かったこと

  • ファイル個別のContent-Lengthがない場合でも、リクエストを最後まで読んでからファイルの長さを確認するのではなく、max-file-sizeを超える量を読み込んだ時点でエラー処理に入っている。
  • max-file-sizeを超える量ぴったりではなく、さらに少しリクエストを読み込んでからエラー処理が行われるのは、リクエストを読み込むStreamのバッファのせい?

スタックトレース

org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 100 bytes.
    at org.apache.tomcat.util.http.fileupload.impl.FileItemStreamImpl$1.raiseError(FileItemStreamImpl.java:114) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.io.FilterInputStream.read(FilterInputStream.java:107) ~[na:na]
    at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:291) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.catalina.connector.Request.parseParts(Request.java:2922) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.Request.getParts(Request.java:2824) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.RequestFacade.getParts(RequestFacade.java:1098) ~[catalina.jar:9.0.44]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:95) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.<init>(StandardMultipartHttpServletRequest.java:88) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:122) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1202) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1036) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[servlet-api.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.9.jar:5.3.9]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[servlet-api.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-websocket.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.9.jar:5.3.9]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.9.jar:5.3.9]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[catalina.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[catalina.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[catalina.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[catalina.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[catalina.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-coyote.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-coyote.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-util.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

*1:spring.servlet.multipart.file-size-thresholdの値を超えたファイルは一時ファイルに出力される。デフォルト値は0なので、すべてのファイルが一時ファイルに保存される。

Validate memory consumption in a JUnit test case

For load test

If you want to run a load test in a JUnit test case and get maximum memory consumption or GC count as a Java variable, the way to do it doesn't seem to be.

For memory consumption of whole Java VM

If you want to run some tests and measure memory consumption of the whole Java VM, you can use Runtime#totalMemory() and Runtime#freeMemory() like this *1:

    static long initialMemoryConsumption;

    @BeforeAll
    static void recordInitialMemoryConsumption() {
        initialMemoryConsumption = getMemoryConsumption();
    }

    @AfterAll
    static void measureWholeMemoryConsumption() {
        System.out.println(getMemoryConsumption() - initialMemoryConsumption);
    }

    static long getMemoryConsumption() {
        Runtime runtime = Runtime.getRuntime();
        runtime.gc();
        return runtime.totalMemory() - runtime.freeMemory();
    }

For memory consumption of an object

If you want to measure rough memory consumption of a specific object in a test (for example, validate memory consumption is  O(n) and not  O(n^{2}) , or validate all the caches are invalidated), you can serialize the object into a byte array and measure the length of it.

    static int getMemoryConsumption(Serializable obj) throws IOException {
        try (
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream out = new ObjectOutputStream(bos);
                ) {
            out.writeObject(obj);
            out.flush();
            byte[] bytes = bos.toByteArray();
            return bytes.length;
        }
    }

    @Test
    void memoryConsumptionTest() throws IOException {
        ...
        assertTrue(getMemoryConsumption(someObject) < 500);
    }
  • You cannot use Runtime#totalMemory() and Runtime#freeMemory() for this purpose, because the whole memory consumption of Java VM reduces in a test case (memory consumption could seem like a minus value).
  • The measured object should implement the Serializable interface. If it doesn't, you have to add the interface at runtime with the Java instrumentation API and a library like Javassist *2.
  • The actual length of the byte array (< 500 in the above example) should be measured in the test environment before writing a test case.

文化的まちがいさがし - 上棟式の餅まき有無

  • (A) 上棟式(建前)のときに餅まきをする。
  • (B) 上棟式(建前)のときに餅まきをしない。

有無には地域差が確実にあるはずで、国交省から新設住宅着工戸数も数値が出てるからどこかで集計とかしてないかな……と思ったんだけど、そもそも国交省のデータは県ごとに分かれていなかった。