[Spring MVC]ファイルのダウンロード


1. Intro


Springからファイルをダウンロードする方法を見てみましょう.

2.開発環境

  • spring boot 2.6.2
  • Java 11
  • Gradle 7.3.2
  • Intellij IDEA 2021.2.4 ️
  • 3.ファイルのダウンロード


    ResponseEntity Resourceの使用

    @Controller
    public class DownloadController {
    
      private static final String SAMPLE_FILE_NAME = "스프링.png"; // (1)
    
      @Value("classpath:static/spring.png") // (2)
      private Resource resource;
    
      @GetMapping("/download/img")
      public ResponseEntity<Resource> downloadImg() {
        return ResponseEntity.ok()
            .contentType(MediaType.IMAGE_PNG) // (3)
            .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.inline() // (4)
                .filename(SAMPLE_FILE_NAME, StandardCharsets.UTF_8)
                .build()
                .toString())
            .body(resource);
      }
    
      @GetMapping("/download/file")
      public ResponseEntity<Resource> downloadFile() {
        return ResponseEntity.ok()
            .contentType(MediaType.APPLICATION_OCTET_STREAM) // (5)
            .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment() // (6)
                .filename(SAMPLE_FILE_NAME, StandardCharsets.UTF_8)
                .build()
                .toString())
            .body(resource);
      }
    }

    共通

  • (1)ハングル名のダウンロード処理をテストするために.
  • クラスパス静的フォルダにあるpngファイルをリソースオブジェクトとして
  • (2)に注入する.
  • ブラウザにすぐに表示されるapi(/ダウンロード/img)

  • (3)レンダリングするコンテンツタイプタイトルを指定します.
  • (4)ブラウザで、「コンテンツ-ロケーション」タイトルを「インライン」に指定します.
  • ファイルダウンロードapi(/ダウンロード/file)

  • (5)ブラウザで、ダウンロード機能を実行するためにアプリケーション/octet-streamとして指定します.
  • コンテンツ-ロケーションタイトルを添付ファイルとして指定し、
  • (6)ファイルダウンロードウィンドウで開く.
  • テストコード

    @WebMvcTest(DownloadController.class)
    @Slf4j
    class DownloadControllerTest {
    
      @Autowired
      private MockMvc mockMvc;
    
      @Test
      void downloadImg() throws Exception {
        // when
        MvcResult mvcResult = mockMvc.perform(get("/download/img"))
            .andExpect(status().isOk())
            .andReturn();
    
        // then
        MockHttpServletResponse response = mvcResult.getResponse();
    
        int contentLength = response.getContentLength();
        String contentType = response.getContentType();
        String contentDisposition = response.getHeader(HttpHeaders.CONTENT_DISPOSITION);
    
        assertAll(
            () -> assertThat(contentLength).isEqualTo(9183),
            () -> assertThat(contentType).isEqualTo(MediaType.IMAGE_PNG_VALUE),
            () -> assertThat(contentDisposition).contains("inline", "UTF-8")
        );
    
        // inline; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%81.png
        log.info("contentDisposition : {}", contentDisposition);
      }
    
      @Test
      void downloadFile() throws Exception {
        // when
        MvcResult mvcResult = mockMvc.perform(get("/download/file"))
            .andExpect(status().isOk())
            .andReturn();
    
        // then
        MockHttpServletResponse response = mvcResult.getResponse();
    
        int contentLength = response.getContentLength();
        String contentType = response.getContentType();
        String contentDisposition = response.getHeader(HttpHeaders.CONTENT_DISPOSITION);
    
        assertAll(
            () -> assertThat(contentLength).isEqualTo(9183),
            () -> assertThat(contentType).isEqualTo(MediaType.APPLICATION_OCTET_STREAM_VALUE),
            () -> assertThat(contentDisposition).contains("attachment", "UTF-8")
        );
    
        // attachment; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%81.png
        log.info("contentDisposition : {}", contentDisposition);
      }
    }

    Content-Length Header


    コントローラからResponseEntityに戻ると、HttpEntityMethodProcessorはHandleReturnValueで処理されます.AbstractMessageConverterMethodProcessor WriteWithMessageConverterからメッセージ変換器を選択して処理します.
    選択されたメッセージ変換器はResourceHttpMessageConverterです.
    AbstractHttpMessageConverterの実際の書き込み方法では、211行のaddDefaultHeadersが表示されます.

    259行に実コンテンツ長が設定されていない場合は、自動的に追加されます.
    したがって、コントローラにタイトルを個別に設定する必要はありません.

    ファイルのダウンロード時にハングルファイル名を破壊


    従来のブラウザ固有のハングル文字の処理


    複数の既存の例を表示する場合は、次の例を参照してください.
    ファイル名をユーザーエージェントとしてブランチ処理し、ブラウザでエンコードすることがよく見られます.
    public String getBrowser(HttpServletRequest request) {
      String userAgent = request.getHeader("User-Agent");
      
      if (userAgent.contains("MSIE") || userAgent.contains("Trident") || userAgent.contains("Edge")) {
        return "MSIE";
      } else if (userAgent.contains("Chrome")) {
        return "Chrome";
      } else if (userAgent.contains("Opera")) {
        return "Opera";
      } else if (userAgent.contains("Safari")) {
        return "Safari";
      } else if (userAgent.contains("Firefox")) {
        return "Firefox";
      } else {
        return "";
      }
    }
    
    public String encodeFileName(String browser, String filename) {
      if ("MSIE".equals(browser)) {
        return URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
      }
      if ("Firefox".equals(browser)) {
        return // ...
      }
      if ("Chrome".equals(browser)) {
        return // ...
      }
      ...
    }

    RFC 6266,RFC 5987


    異なるブラウザはRFC6266RFC5987の仕様をサポートしています.
    したがって、説明を使用して「コンテンツ-場所」を設定すると、一度に完了できます.
    http://test.greenbytes.de/tech/tc2231.
    IEはIE 9からサポートを開始する.
    TestResultsFF22passMSIE8unsupportedMSIE9passOperapassSaf6passKonqpassChr25pass

    Spring ContentDisposition Class

    Springはまた、使いやすいクラスの処理をサポートする.ContentDispositionでfilenameにcharsetが指定されている場合、タイトルは自動的に対応するspecに符号化されます.
    @Slf4j
    class ContentDispositionTest {
    
      @ParameterizedTest
      @CsvSource({
          "스프링.png, inline; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%81.png",
          "스프링1234.png, inline; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%811234.png",
          "스프링-!@#$%.png, inline; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%81-!%40#$%25.png"
      })
      void buildContentDisposition(String filename, String expected) {
        // when
        ContentDisposition contentDisposition = ContentDisposition.inline()
            .filename(filename, StandardCharsets.UTF_8)
            .build();
    
        // then
        assertThat(contentDisposition.toString()).isEqualTo(expected);
      }
    }

    コンテンツ→位置見出し解析

    Content-Disposition: inline; filename*=UTF-8''%EC%8A%A4%ED%94%84%EB%A7%81.png
    RFC2231の4.Parameter Value Character SetおよびLanguage Informationを参照してください.
    パラメータ値に文字セットまたは言語を指定する必要がある場合は、アステリーリスク(*)を加え、'を区切り記号とします.
    文字セット、言語、値の順に設定すればいいです.
  • ex 1) title*=us-ascii'en-us'value
  • ex 2) filename*=UTF-8''value
  • 値はURLでエンコードされます.
    最後に、文字セットまたは言語は空にすることができますが、'を表示する必要があります.

    4.終了


    ブログで使用されているコードはGithubで見つけることができます.

    5.「」を参照してください。

  • MDN, Content-Disposition
  • ファイルダウンロードファイル名の競合