Spring MultipartFile アップロードが完了しないとControllerのメソッドは呼ばれないぞ!

MuitipartFileとは?

MultipartFileとは、ファイルのアップロード機能をアプリケーションコード内で透過的に扱うためのクラスです。アップロード機能の実装は、Servlet 3.0のファイルアップロードか、Apache Commons Fileuploadから選ぶことができます。

具体的な解説および実装方法は以下が参考になります。
参考 TERASOLUNA Server Framework for Java (5.x) Development Guideline 4.9. ファイルアップロード

以下、Servlet 3.0のファイルアップロード機能を前提とします。

さて

突然ですが、問題です。

以下のようなファイルアップロードのコードがあります。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
@Slf4j
@RestController
@SpringBootApplication
public class MultipartApplication {

  public static void main(String[] args) {
    SpringApplication.run(MultipartApplication.class, args);
  }

  @PostMapping("upload")
  public void upload(@RequestParam("file") MultipartFile file) {
    log.info(file.getOriginalFilename());
  }
}

/resources/static/index.html

<!doctype html>
<html>
  <body>
    <h1>upload sample</h1>
    <form action="/upload" method="POST" enctype="multipart/form-data">
      <input type="file" name="file">
      <input type="submit">
    </form>
  </body>
</html>

このとき、uploadメソッドはいつ呼ばれるでしょうか?

  1. アップロードが開始したとき
  2. アップロードが全て完了したとき

ヒント1

シンプルなServletを実装してHttpServletRequest#getInputStream()でデータを読み込んでみると、

@WebServlet(urlPatterns = "/upload")
public class SampleServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  // ... 省略
  }

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    InputStream in = req.getInputStream();
    int i =0;
    while((i = in.read()) != -1) {
      System.out.print((char)i);
    }
  }
}

アップロード開始したときからdoPostの処理が実行されます。

f:id:kimulla:20191203223731g:plain

ヒント2

MultipartFileはgetInputStream()を持っています。少しずつデータを渡してくれるということは… MultipartFile#getInputStream Javadoc

答え

アップロードが全て完了したときです。

f:id:kimulla:20191203224524g:plain

なんでやねん

今回はファイルアップロードの実装としてServlet3.0のファイルアップロード機能を利用しています。そのServletの挙動として、HttpServletRequest#getPart() を実行すると、アップロードが完了するまで(あるいはアップロードサイズなどの上限に達するまて)待ちます。

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // アップロードが完了するまで止まる
    Part part = req.getPart("file");
  }

Spring Frameworkでは、Controllerのハンドラ実行前に引数を解決します。 このとき、HttpServletRequest#getParts()が実行されます。

flow TERASOLUNA Server Framework for Java (5.x) Development Guideline 4.9. ファイルアップロード

f:id:kimulla:20191203222100j:plain

そのため、アップロードが完了しないとControllerのメソッドは実行されません。

そもそもHttpServletRequest#getParts()がブロックしなければいい話ですが、APサーバとしてファイルサイズの上限をチェックする機能を提供しているので、ファイルを読み込むまではファイルサイズが確定しないため、仕方ありません。

まとめ

Spring Frameworkだけじゃなくて、その下も勉強しよう!

そして何より、雰囲気でAPIの裏側まで想像したらアカン!