1. 概要

このチュートリアルでは、RestTemplateを使用して大きなファイルをダウンロードする方法に関するさまざまな手法を紹介します。

2.  RestTemplate

RestTemplate は、Spring3で導入されたブロッキングおよび同期HTTPクライアントです。 Springのドキュメントによると、バージョン5でリアクティブなノンブロッキングHTTPクライアントとして WebClient が導入されたため、将来的に非推奨になります。

3. 落とし穴

通常、ファイルをダウンロードするときは、ファイルシステムに保存するか、バイト配列としてメモリにロードします。 ただし、ファイルが大きい場合、メモリ内のロードによりOutOfMemoryErrorが発生する可能性があります。 したがって、応答のチャンクを読み取るときに、データをファイルに保存する必要があります。

まず、機能しないいくつかの方法を見てみましょう。

まず、リターンタイプとしてResourceを返すとどうなりますか。

Resource download() {
    return new ClassPathResource(locationForLargeFile);
}

これが機能しない理由は、 ResourceHttpMesssageConverter が応答本文全体をByteArrayInputStreamにロードし、回避したいメモリプレッシャーを追加するためです。

次に、 InputStreamResource を返し、 ResourceHttpMessageConverter#supportsReadStreamingを構成するとどうなりますか? InputStreamResource.getInputStream()を呼び出すことができるようになるまでに、「ソケットが閉じられました」エラーが発生するため、これも機能しません!これは、「execute」が閉じるためです。出口の前の応答入力ストリーム。

では、問題を解決するために何ができるでしょうか。 実際、ここにも2つのことがあります。

  • リターンタイプとしてファイルをサポートするカスタムHttpMessageConverterを記述します
  • RestTemplate.execute カスタムResponseExtractorを使用して、入力ストリームをファイルに保存します。

このチュートリアルでは、2番目のソリューションを使用します。これは、柔軟性が高く、労力も少ないためです。

4. 履歴書なしでダウンロード

ResponseExtractor を実装して、本文を一時ファイルに書き込みましょう

File file = restTemplate.execute(FILE_URL, HttpMethod.GET, null, clientHttpResponse -> {
    File ret = File.createTempFile("download", "tmp");
    StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret));
    return ret;
});

Assert.assertNotNull(file);
Assertions
  .assertThat(file.length())
  .isEqualTo(contentLength);

ここでは、 StreamUtils.copy を使用して、応答入力ストリームを FileOutputStream、にコピーしましたが、他のテクニックとライブラリも利用できます。

5. 一時停止して再開してダウンロード

大きなファイルをダウンロードするので、何らかの理由で一時停止した後でダウンロードすることを検討するのが妥当です。

それでは、最初にダウンロードURLが再開をサポートしているかどうかを確認しましょう。

HttpHeaders headers = restTemplate.headForHeaders(FILE_URL);

Assertions
  .assertThat(headers.get("Accept-Ranges"))
  .contains("bytes");
Assertions
  .assertThat(headers.getContentLength())
  .isGreaterThan(0);

次に、 RequestCallback を実装して、「Range」ヘッダーを設定し、ダウンロードを再開できます。

restTemplate.execute(
  FILE_URL,
  HttpMethod.GET,
  clientHttpRequest -> clientHttpRequest.getHeaders().set(
    "Range",
    String.format("bytes=%d-%d", file.length(), contentLength)),
    clientHttpResponse -> {
        StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(file, true));
    return file;
});

Assertions
  .assertThat(file.length())
  .isLessThanOrEqualTo(contentLength);

正確なコンテンツの長さがわからない場合は、String.formatを使用してRangeヘッダー値を設定できます。

String.format("bytes=%d-", file.length())

6. 結論

大きなファイルをダウンロードするときに発生する可能性のある問題について説明しました。 また、RestTemplateを使用した場合の解決策も示しました。 最後に、再開可能なダウンロードを実装する方法を示しました。

いつものように、コードはGitHubで入手できます。